@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.
- package/.cache/eleventy-fetch-68a403136c61abd104ade94e69ba62 +1 -1
- package/eleventy-cache-webmentions.cjs +677 -0
- package/eleventy-cache-webmentions.js +187 -141
- package/eleventy-cache-webmentions.min.cjs +1 -0
- package/eleventy-cache-webmentions.min.js +1 -0
- package/eslint.config.js +30 -10
- package/package.json +12 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"cachedAt":
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { AssetCache } from "@11ty/eleventy-fetch";
|
|
2
|
+
import { styleText } from "node:util";
|
|
3
|
+
import sanitizeHTML from "sanitize-html";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* @
|
|
6
|
+
* @typedef {sanitizeHTML.IOptions} AllowedHTML
|
|
7
7
|
*/
|
|
8
|
-
|
|
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: ["
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
181
|
-
* @param {
|
|
182
|
-
* @returns {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
308
|
+
* @param {Webmention[]} webmentions
|
|
245
309
|
* @param {string[]} allowlist
|
|
246
|
-
* @returns {
|
|
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 {
|
|
266
|
-
* @param {
|
|
331
|
+
* @param {Options} options
|
|
332
|
+
* @param {Webmention[]} webmentions
|
|
267
333
|
* @param {string} url
|
|
268
|
-
* @returns {Promise<{found:number,
|
|
334
|
+
* @returns {Promise<{found: number, webmentions: Webmention[]}>}
|
|
269
335
|
*/
|
|
270
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
webmentions: webmentions,
|
|
328
394
|
};
|
|
329
395
|
});
|
|
330
396
|
};
|
|
331
397
|
|
|
332
398
|
/**
|
|
333
|
-
* @param {
|
|
334
|
-
* @returns {Promise<
|
|
399
|
+
* @param {Options} options
|
|
400
|
+
* @returns {Promise<Webmention[]>}
|
|
335
401
|
*/
|
|
336
|
-
const
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
428
|
-
webmentions = fetched.
|
|
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
|
-
|
|
532
|
+
/** @type {Webmention[]} */
|
|
533
|
+
const WEBMENTIONS = {};
|
|
467
534
|
|
|
468
535
|
/**
|
|
469
|
-
* @param {
|
|
470
|
-
* @returns {Promise<Object<string,
|
|
536
|
+
* @param {Options} options
|
|
537
|
+
* @returns {Promise<Object<string, Webmention[]>>}
|
|
471
538
|
*/
|
|
472
|
-
const
|
|
473
|
-
if (Object.
|
|
474
|
-
return
|
|
539
|
+
export const webmentionsByURL = async (options) => {
|
|
540
|
+
if (Object.keys(WEBMENTIONS).length) {
|
|
541
|
+
return WEBMENTIONS;
|
|
475
542
|
}
|
|
476
543
|
|
|
477
|
-
let rawWebmentions = await
|
|
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 =
|
|
483
|
-
|
|
549
|
+
let url = baseURL(
|
|
550
|
+
fixURL(
|
|
484
551
|
getTarget(webmention).replace(/\/?$/, "/"),
|
|
485
552
|
options.urlReplacements,
|
|
486
553
|
),
|
|
487
554
|
);
|
|
488
555
|
|
|
489
|
-
if (!
|
|
490
|
-
|
|
556
|
+
if (!WEBMENTIONS[url]) {
|
|
557
|
+
WEBMENTIONS[url] = [];
|
|
491
558
|
}
|
|
492
559
|
|
|
493
|
-
|
|
560
|
+
WEBMENTIONS[url].push(webmention);
|
|
494
561
|
});
|
|
495
562
|
|
|
496
|
-
return
|
|
563
|
+
return WEBMENTIONS;
|
|
497
564
|
};
|
|
565
|
+
export const webmentionsByUrl = webmentionsByURL;
|
|
566
|
+
export const filteredWebmentions = webmentionsByURL;
|
|
498
567
|
|
|
499
568
|
/**
|
|
500
|
-
* @param {
|
|
569
|
+
* @param {Options} options
|
|
501
570
|
* @param {string} url
|
|
502
|
-
* @param {
|
|
503
|
-
* @returns {Promise<
|
|
571
|
+
* @param {WebmentionType[]|WebmentionType} [types=[]]
|
|
572
|
+
* @returns {Promise<Webmention[]>}
|
|
504
573
|
*/
|
|
505
|
-
const getWebmentions = async (options, url, types =
|
|
506
|
-
const webmentions = await
|
|
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 {
|
|
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
|
-
|
|
559
|
-
eleventyConfig.addGlobalData("webmentionsByUrl",
|
|
560
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
2
5
|
{
|
|
3
|
-
files: ["
|
|
6
|
+
files: ["src/**/*.js"],
|
|
4
7
|
languageOptions: {
|
|
5
|
-
ecmaVersion:
|
|
6
|
-
sourceType: "module",
|
|
8
|
+
ecmaVersion: "latest",
|
|
7
9
|
},
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|