451-tools
Version:
Censorship resilient and distributed publishing: informed by the needs of publishers and their audiences, More Mirrors implements a set of offline fallback strategies for censorship resilient websites.
989 lines • 92.4 kB
JavaScript
var Z = (e) => {
throw TypeError(e);
};
var ee = (e, t, r) => t.has(e) || Z("Cannot " + r);
var D = (e, t, r) => (ee(e, t, "read from private field"), r ? r.call(e) : t.get(e)), te = (e, t, r) => t.has(e) ? Z("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, r), L = (e, t, r, n) => (ee(e, t, "write to private field"), n ? n.call(e, r) : t.set(e, r), r);
try {
self["workbox:core:7.0.0"] && _();
} catch {
}
const ke = {
"invalid-value": ({ paramName: e, validValueDescription: t, value: r }) => {
if (!e || !t)
throw new Error("Unexpected input to 'invalid-value' error.");
return `The '${e}' parameter was given a value with an unexpected value. ${t} Received a value of ${JSON.stringify(r)}.`;
},
"not-an-array": ({ moduleName: e, className: t, funcName: r, paramName: n }) => {
if (!e || !t || !r || !n)
throw new Error("Unexpected input to 'not-an-array' error.");
return `The parameter '${n}' passed into '${e}.${t}.${r}()' must be an array.`;
},
"incorrect-type": ({ expectedType: e, paramName: t, moduleName: r, className: n, funcName: s }) => {
if (!e || !t || !r || !s)
throw new Error("Unexpected input to 'incorrect-type' error.");
const a = n ? `${n}.` : "";
return `The parameter '${t}' passed into '${r}.${a}${s}()' must be of type ${e}.`;
},
"incorrect-class": ({ expectedClassName: e, paramName: t, moduleName: r, className: n, funcName: s, isReturnValueProblem: a }) => {
if (!e || !r || !s)
throw new Error("Unexpected input to 'incorrect-class' error.");
const o = n ? `${n}.` : "";
return a ? `The return value from '${r}.${o}${s}()' must be an instance of class ${e}.` : `The parameter '${t}' passed into '${r}.${o}${s}()' must be an instance of class ${e}.`;
},
"missing-a-method": ({ expectedMethod: e, paramName: t, moduleName: r, className: n, funcName: s }) => {
if (!e || !t || !r || !n || !s)
throw new Error("Unexpected input to 'missing-a-method' error.");
return `${r}.${n}.${s}() expected the '${t}' parameter to expose a '${e}' method.`;
},
"add-to-cache-list-unexpected-type": ({ entry: e }) => `An unexpected entry was passed to 'workbox-precaching.PrecacheController.addToCacheList()' The entry '${JSON.stringify(e)}' isn't supported. You must supply an array of strings with one or more characters, objects with a url property or Request objects.`,
"add-to-cache-list-conflicting-entries": ({ firstEntry: e, secondEntry: t }) => {
if (!e || !t)
throw new Error("Unexpected input to 'add-to-cache-list-duplicate-entries' error.");
return `Two of the entries passed to 'workbox-precaching.PrecacheController.addToCacheList()' had the URL ${e} but different revision details. Workbox is unable to cache and version the asset correctly. Please remove one of the entries.`;
},
"plugin-error-request-will-fetch": ({ thrownErrorMessage: e }) => {
if (!e)
throw new Error("Unexpected input to 'plugin-error-request-will-fetch', error.");
return `An error was thrown by a plugins 'requestWillFetch()' method. The thrown error message was: '${e}'.`;
},
"invalid-cache-name": ({ cacheNameId: e, value: t }) => {
if (!e)
throw new Error("Expected a 'cacheNameId' for error 'invalid-cache-name'");
return `You must provide a name containing at least one character for setCacheDetails({${e}: '...'}). Received a value of '${JSON.stringify(t)}'`;
},
"unregister-route-but-not-found-with-method": ({ method: e }) => {
if (!e)
throw new Error("Unexpected input to 'unregister-route-but-not-found-with-method' error.");
return `The route you're trying to unregister was not previously registered for the method type '${e}'.`;
},
"unregister-route-route-not-registered": () => "The route you're trying to unregister was not previously registered.",
"queue-replay-failed": ({ name: e }) => `Replaying the background sync queue '${e}' failed.`,
"duplicate-queue-name": ({ name: e }) => `The Queue name '${e}' is already being used. All instances of backgroundSync.Queue must be given unique names.`,
"expired-test-without-max-age": ({ methodName: e, paramName: t }) => `The '${e}()' method can only be used when the '${t}' is used in the constructor.`,
"unsupported-route-type": ({ moduleName: e, className: t, funcName: r, paramName: n }) => `The supplied '${n}' parameter was an unsupported type. Please check the docs for ${e}.${t}.${r} for valid input types.`,
"not-array-of-class": ({ value: e, expectedClass: t, moduleName: r, className: n, funcName: s, paramName: a }) => `The supplied '${a}' parameter must be an array of '${t}' objects. Received '${JSON.stringify(e)},'. Please check the call to ${r}.${n}.${s}() to fix the issue.`,
"max-entries-or-age-required": ({ moduleName: e, className: t, funcName: r }) => `You must define either config.maxEntries or config.maxAgeSecondsin ${e}.${t}.${r}`,
"statuses-or-headers-required": ({ moduleName: e, className: t, funcName: r }) => `You must define either config.statuses or config.headersin ${e}.${t}.${r}`,
"invalid-string": ({ moduleName: e, funcName: t, paramName: r }) => {
if (!r || !e || !t)
throw new Error("Unexpected input to 'invalid-string' error.");
return `When using strings, the '${r}' parameter must start with 'http' (for cross-origin matches) or '/' (for same-origin matches). Please see the docs for ${e}.${t}() for more info.`;
},
"channel-name-required": () => "You must provide a channelName to construct a BroadcastCacheUpdate instance.",
"invalid-responses-are-same-args": () => "The arguments passed into responsesAreSame() appear to be invalid. Please ensure valid Responses are used.",
"expire-custom-caches-only": () => "You must provide a 'cacheName' property when using the expiration plugin with a runtime caching strategy.",
"unit-must-be-bytes": ({ normalizedRangeHeader: e }) => {
if (!e)
throw new Error("Unexpected input to 'unit-must-be-bytes' error.");
return `The 'unit' portion of the Range header must be set to 'bytes'. The Range header provided was "${e}"`;
},
"single-range-only": ({ normalizedRangeHeader: e }) => {
if (!e)
throw new Error("Unexpected input to 'single-range-only' error.");
return `Multiple ranges are not supported. Please use a single start value, and optional end value. The Range header provided was "${e}"`;
},
"invalid-range-values": ({ normalizedRangeHeader: e }) => {
if (!e)
throw new Error("Unexpected input to 'invalid-range-values' error.");
return `The Range header is missing both start and end values. At least one of those values is needed. The Range header provided was "${e}"`;
},
"no-range-header": () => "No Range header was found in the Request provided.",
"range-not-satisfiable": ({ size: e, start: t, end: r }) => `The start (${t}) and end (${r}) values in the Range are not satisfiable by the cached response, which is ${e} bytes.`,
"attempt-to-cache-non-get-request": ({ url: e, method: t }) => `Unable to cache '${e}' because it is a '${t}' request and only 'GET' requests can be cached.`,
"cache-put-with-no-response": ({ url: e }) => `There was an attempt to cache '${e}' but the response was not defined.`,
"no-response": ({ url: e, error: t }) => {
let r = `The strategy could not generate a response for '${e}'.`;
return t && (r += ` The underlying error is ${t}.`), r;
},
"bad-precaching-response": ({ url: e, status: t }) => `The precaching request for '${e}' failed` + (t ? ` with an HTTP status of ${t}.` : "."),
"non-precached-url": ({ url: e }) => `createHandlerBoundToURL('${e}') was called, but that URL is not precached. Please pass in a URL that is precached instead.`,
"add-to-cache-list-conflicting-integrities": ({ url: e }) => `Two of the entries passed to 'workbox-precaching.PrecacheController.addToCacheList()' had the URL ${e} with different integrity values. Please remove one of them.`,
"missing-precache-entry": ({ cacheName: e, url: t }) => `Unable to find a precached response in ${e} for ${t}.`,
"cross-origin-copy-response": ({ origin: e }) => `workbox-core.copyResponse() can only be used with same-origin responses. It was passed a response with origin ${e}.`,
"opaque-streams-source": ({ type: e }) => {
const t = `One of the workbox-streams sources resulted in an '${e}' response.`;
return e === "opaqueredirect" ? `${t} Please do not use a navigation request that results in a redirect as a source.` : `${t} Please ensure your sources are CORS-enabled.`;
}
}, xe = (e, ...t) => {
let r = e;
return t.length > 0 && (r += ` :: ${JSON.stringify(t)}`), r;
}, Ee = (e, t = {}) => {
const r = ke[e];
if (!r)
throw new Error(`Unable to find message for code '${e}'.`);
return r(t);
}, Te = process.env.NODE_ENV === "production" ? xe : Ee;
let k = class extends Error {
/**
*
* @param {string} errorCode The error code that
* identifies this particular error.
* @param {Object=} details Any relevant arguments
* that will help developers identify issues should
* be added as a key on the context object.
*/
constructor(t, r) {
const n = Te(t, r);
super(n), this.name = t, this.details = r;
}
};
const $e = (e, t) => {
if (!Array.isArray(e))
throw new k("not-an-array", t);
}, ve = (e, t, r) => {
if (typeof e[t] !== "function")
throw r.expectedMethod = t, new k("missing-a-method", r);
}, Re = (e, t, r) => {
if (typeof e !== t)
throw r.expectedType = t, new k("incorrect-type", r);
}, Ce = (e, t, r) => {
if (!(e instanceof t))
throw r.expectedClassName = t.name, new k("incorrect-class", r);
}, Ne = (e, t, r) => {
if (!t.includes(e))
throw r.validValueDescription = `Valid values are ${JSON.stringify(t)}.`, new k("invalid-value", r);
}, _e = (e, t, r) => {
const n = new k("not-array-of-class", r);
if (!Array.isArray(e))
throw n;
for (const s of e)
if (!(s instanceof t))
throw n;
}, M = process.env.NODE_ENV === "production" ? null : {
hasMethod: ve,
isArray: $e,
isInstance: Ce,
isOneOf: Ne,
isType: Re,
isArrayOfClass: _e
};
function Ae(e) {
e.then(() => {
});
}
const R = process.env.NODE_ENV === "production" ? null : (() => {
"__WB_DISABLE_DEV_LOGS" in globalThis || (self.__WB_DISABLE_DEV_LOGS = !1);
let e = !1;
const t = {
debug: "#7f8c8d",
log: "#2ecc71",
warn: "#f39c12",
error: "#c0392b",
groupCollapsed: "#3498db",
groupEnd: null
// No colored prefix on groupEnd
}, r = function(a, o) {
if (self.__WB_DISABLE_DEV_LOGS)
return;
if (a === "groupCollapsed" && /^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
console[a](...o);
return;
}
const i = [
`background: ${t[a]}`,
"border-radius: 0.5em",
"color: white",
"font-weight: bold",
"padding: 2px 0.5em"
], c = e ? [] : ["%cworkbox", i.join(";")];
console[a](...c, ...o), a === "groupCollapsed" && (e = !0), a === "groupEnd" && (e = !1);
}, n = {}, s = Object.keys(t);
for (const a of s) {
const o = a;
n[o] = (...i) => {
r(o, i);
};
}
return n;
})(), De = (e, t) => t.some((r) => e instanceof r);
let re, ne;
function Le() {
return re || (re = [
IDBDatabase,
IDBObjectStore,
IDBIndex,
IDBCursor,
IDBTransaction
]);
}
function Me() {
return ne || (ne = [
IDBCursor.prototype.advance,
IDBCursor.prototype.continue,
IDBCursor.prototype.continuePrimaryKey
]);
}
const le = /* @__PURE__ */ new WeakMap(), j = /* @__PURE__ */ new WeakMap(), de = /* @__PURE__ */ new WeakMap(), H = /* @__PURE__ */ new WeakMap(), W = /* @__PURE__ */ new WeakMap();
function Pe(e) {
const t = new Promise((r, n) => {
const s = () => {
e.removeEventListener("success", a), e.removeEventListener("error", o);
}, a = () => {
r(w(e.result)), s();
}, o = () => {
n(e.error), s();
};
e.addEventListener("success", a), e.addEventListener("error", o);
});
return t.then((r) => {
r instanceof IDBCursor && le.set(r, e);
}).catch(() => {
}), W.set(t, e), t;
}
function Oe(e) {
if (j.has(e))
return;
const t = new Promise((r, n) => {
const s = () => {
e.removeEventListener("complete", a), e.removeEventListener("error", o), e.removeEventListener("abort", o);
}, a = () => {
r(), s();
}, o = () => {
n(e.error || new DOMException("AbortError", "AbortError")), s();
};
e.addEventListener("complete", a), e.addEventListener("error", o), e.addEventListener("abort", o);
});
j.set(e, t);
}
let V = {
get(e, t, r) {
if (e instanceof IDBTransaction) {
if (t === "done")
return j.get(e);
if (t === "objectStoreNames")
return e.objectStoreNames || de.get(e);
if (t === "store")
return r.objectStoreNames[1] ? void 0 : r.objectStore(r.objectStoreNames[0]);
}
return w(e[t]);
},
set(e, t, r) {
return e[t] = r, !0;
},
has(e, t) {
return e instanceof IDBTransaction && (t === "done" || t === "store") ? !0 : t in e;
}
};
function qe(e) {
V = e(V);
}
function Se(e) {
return e === IDBDatabase.prototype.transaction && !("objectStoreNames" in IDBTransaction.prototype) ? function(t, ...r) {
const n = e.call(I(this), t, ...r);
return de.set(n, t.sort ? t.sort() : [t]), w(n);
} : Me().includes(e) ? function(...t) {
return e.apply(I(this), t), w(le.get(this));
} : function(...t) {
return w(e.apply(I(this), t));
};
}
function Ue(e) {
return typeof e == "function" ? Se(e) : (e instanceof IDBTransaction && Oe(e), De(e, Le()) ? new Proxy(e, V) : e);
}
function w(e) {
if (e instanceof IDBRequest)
return Pe(e);
if (H.has(e))
return H.get(e);
const t = Ue(e);
return t !== e && (H.set(e, t), W.set(t, e)), t;
}
const I = (e) => W.get(e);
function He(e, t, { blocked: r, upgrade: n, blocking: s, terminated: a } = {}) {
const o = indexedDB.open(e, t), i = w(o);
return n && o.addEventListener("upgradeneeded", (c) => {
n(w(o.result), c.oldVersion, c.newVersion, w(o.transaction), c);
}), r && o.addEventListener("blocked", (c) => r(
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
c.oldVersion,
c.newVersion,
c
)), i.then((c) => {
a && c.addEventListener("close", () => a()), s && c.addEventListener("versionchange", (l) => s(l.oldVersion, l.newVersion, l));
}).catch(() => {
}), i;
}
function Ie(e, { blocked: t } = {}) {
const r = indexedDB.deleteDatabase(e);
return t && r.addEventListener("blocked", (n) => t(
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
n.oldVersion,
n
)), w(r).then(() => {
});
}
const Be = ["get", "getKey", "getAll", "getAllKeys", "count"], Fe = ["put", "add", "delete", "clear"], B = /* @__PURE__ */ new Map();
function se(e, t) {
if (!(e instanceof IDBDatabase && !(t in e) && typeof t == "string"))
return;
if (B.get(t))
return B.get(t);
const r = t.replace(/FromIndex$/, ""), n = t !== r, s = Fe.includes(r);
if (
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
!(r in (n ? IDBIndex : IDBObjectStore).prototype) || !(s || Be.includes(r))
)
return;
const a = async function(o, ...i) {
const c = this.transaction(o, s ? "readwrite" : "readonly");
let l = c.store;
return n && (l = l.index(i.shift())), (await Promise.all([
l[r](...i),
s && c.done
]))[0];
};
return B.set(t, a), a;
}
qe((e) => ({
...e,
get: (t, r, n) => se(t, r) || e.get(t, r, n),
has: (t, r) => !!se(t, r) || e.has(t, r)
}));
try {
self["workbox:expiration:7.0.0"] && _();
} catch {
}
const je = "workbox-expiration", C = "cache-entries", ae = (e) => {
const t = new URL(e, location.href);
return t.hash = "", t.href;
};
class Ve {
/**
*
* @param {string} cacheName
*
* @private
*/
constructor(t) {
this._db = null, this._cacheName = t;
}
/**
* Performs an upgrade of indexedDB.
*
* @param {IDBPDatabase<CacheDbSchema>} db
*
* @private
*/
_upgradeDb(t) {
const r = t.createObjectStore(C, { keyPath: "id" });
r.createIndex("cacheName", "cacheName", { unique: !1 }), r.createIndex("timestamp", "timestamp", { unique: !1 });
}
/**
* Performs an upgrade of indexedDB and deletes deprecated DBs.
*
* @param {IDBPDatabase<CacheDbSchema>} db
*
* @private
*/
_upgradeDbAndDeleteOldDbs(t) {
this._upgradeDb(t), this._cacheName && Ie(this._cacheName);
}
/**
* @param {string} url
* @param {number} timestamp
*
* @private
*/
async setTimestamp(t, r) {
t = ae(t);
const n = {
url: t,
timestamp: r,
cacheName: this._cacheName,
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
id: this._getId(t)
}, a = (await this.getDb()).transaction(C, "readwrite", {
durability: "relaxed"
});
await a.store.put(n), await a.done;
}
/**
* Returns the timestamp stored for a given URL.
*
* @param {string} url
* @return {number | undefined}
*
* @private
*/
async getTimestamp(t) {
const n = await (await this.getDb()).get(C, this._getId(t));
return n == null ? void 0 : n.timestamp;
}
/**
* Iterates through all the entries in the object store (from newest to
* oldest) and removes entries once either `maxCount` is reached or the
* entry's timestamp is less than `minTimestamp`.
*
* @param {number} minTimestamp
* @param {number} maxCount
* @return {Array<string>}
*
* @private
*/
async expireEntries(t, r) {
const n = await this.getDb();
let s = await n.transaction(C).store.index("timestamp").openCursor(null, "prev");
const a = [];
let o = 0;
for (; s; ) {
const c = s.value;
c.cacheName === this._cacheName && (t && c.timestamp < t || r && o >= r ? a.push(s.value) : o++), s = await s.continue();
}
const i = [];
for (const c of a)
await n.delete(C, c.id), i.push(c.url);
return i;
}
/**
* Takes a URL and returns an ID that will be unique in the object store.
*
* @param {string} url
* @return {string}
*
* @private
*/
_getId(t) {
return this._cacheName + "|" + ae(t);
}
/**
* Returns an open connection to the database.
*
* @private
*/
async getDb() {
return this._db || (this._db = await He(je, 1, {
upgrade: this._upgradeDbAndDeleteOldDbs.bind(this)
})), this._db;
}
}
class J {
/**
* To construct a new CacheExpiration instance you must provide at least
* one of the `config` properties.
*
* @param {string} cacheName Name of the cache to apply restrictions to.
* @param {Object} config
* @param {number} [config.maxEntries] The maximum number of entries to cache.
* Entries used the least will be removed as the maximum is reached.
* @param {number} [config.maxAgeSeconds] The maximum age of an entry before
* it's treated as stale and removed.
* @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters)
* that will be used when calling `delete()` on the cache.
*/
constructor(t, r = {}) {
if (this._isRunning = !1, this._rerunRequested = !1, process.env.NODE_ENV !== "production") {
if (M.isType(t, "string", {
moduleName: "workbox-expiration",
className: "CacheExpiration",
funcName: "constructor",
paramName: "cacheName"
}), !(r.maxEntries || r.maxAgeSeconds))
throw new k("max-entries-or-age-required", {
moduleName: "workbox-expiration",
className: "CacheExpiration",
funcName: "constructor"
});
r.maxEntries && M.isType(r.maxEntries, "number", {
moduleName: "workbox-expiration",
className: "CacheExpiration",
funcName: "constructor",
paramName: "config.maxEntries"
}), r.maxAgeSeconds && M.isType(r.maxAgeSeconds, "number", {
moduleName: "workbox-expiration",
className: "CacheExpiration",
funcName: "constructor",
paramName: "config.maxAgeSeconds"
});
}
this._maxEntries = r.maxEntries, this._maxAgeSeconds = r.maxAgeSeconds, this._matchOptions = r.matchOptions, this._cacheName = t, this._timestampModel = new Ve(t);
}
/**
* Expires entries for the given cache and given criteria.
*/
async expireEntries() {
if (this._isRunning) {
this._rerunRequested = !0;
return;
}
this._isRunning = !0;
const t = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1e3 : 0, r = await this._timestampModel.expireEntries(t, this._maxEntries), n = await self.caches.open(this._cacheName);
for (const s of r)
await n.delete(s, this._matchOptions);
process.env.NODE_ENV !== "production" && (r.length > 0 ? (R.groupCollapsed(`Expired ${r.length} ${r.length === 1 ? "entry" : "entries"} and removed ${r.length === 1 ? "it" : "them"} from the '${this._cacheName}' cache.`), R.log(`Expired the following ${r.length === 1 ? "URL" : "URLs"}:`), r.forEach((s) => R.log(` ${s}`)), R.groupEnd()) : R.debug("Cache expiration ran and found no entries to remove.")), this._isRunning = !1, this._rerunRequested && (this._rerunRequested = !1, Ae(this.expireEntries()));
}
/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
* is accurate or when expiring, the timestamp is up-to-date.
*
* @param {string} url
*/
async updateTimestamp(t) {
process.env.NODE_ENV !== "production" && M.isType(t, "string", {
moduleName: "workbox-expiration",
className: "CacheExpiration",
funcName: "updateTimestamp",
paramName: "url"
}), await this._timestampModel.setTimestamp(t, Date.now());
}
/**
* Can be used to check if a URL has expired or not before it's used.
*
* This requires a look up from IndexedDB, so can be slow.
*
* Note: This method will not remove the cached entry, call
* `expireEntries()` to remove indexedDB and Cache entries.
*
* @param {string} url
* @return {boolean}
*/
async isURLExpired(t) {
if (this._maxAgeSeconds) {
const r = await this._timestampModel.getTimestamp(t), n = Date.now() - this._maxAgeSeconds * 1e3;
return r !== void 0 ? r < n : !0;
} else {
if (process.env.NODE_ENV !== "production")
throw new k("expired-test-without-max-age", {
methodName: "isURLExpired",
paramName: "maxAgeSeconds"
});
return !1;
}
}
/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
async delete() {
this._rerunRequested = !1, await this._timestampModel.expireEntries(1 / 0);
}
}
typeof registration < "u" && registration.scope;
const u = "451-tools", G = ["network-only", "network-first", "stale-while-revalidate"], he = {
"network-only": Ge,
"network-first": Y,
"stale-while-revalidate": We
};
function Ge({ event: e, fetcher: t = fetch }) {
const { request: r } = e;
return t(r);
}
function Y({
cacheName: e,
event: t,
expirationManager: r,
fetcher: n = fetch
}) {
const { request: s } = t;
return n(s).then((a) => {
const o = a.clone();
return a.ok && caches.open(e).then((i) => i.put(s, o)).then(async () => {
r && (await r.updateTimestamp(s.url), await r.expireEntries());
}), a;
}).catch(
() => caches.open(e).then((a) => a.match(s, { ignoreSearch: !0 }))
);
}
function We({
cacheName: e,
event: t,
expirationManager: r,
fetcher: n = fetch
}) {
const { request: s } = t;
return caches.open(e).then((a) => a.match(s, { ignoreSearch: !0 })).then(async (a) => a ? (t.waitUntil(
n(s).then(async (o) => {
if (o.ok) {
const i = o.clone();
caches.open(e).then((c) => c.put(s.url, i)).then(async () => {
r && (await r.updateTimestamp(s.url), await r.expireEntries());
});
}
return o;
})
), a) : n(s).then((o) => o.ok ? caches.open(e).then((i) => i.put(s, o.clone())).then(() => o) : o));
}
function Je(e) {
const t = JSON.stringify(e);
return self.clients.matchAll().then((r) => Promise.all(
r.map((n) => (n.postMessage(t), Promise.resolve("postMessage sent, ", t)))
));
}
const pe = "mirrors", fe = "/mirrors.json", Ye = 10 * 60 * 1e3, x = "up", z = "down";
async function S() {
return caches.open(pe).then((e) => e.match(fe)).then((e) => e.json());
}
async function X(e) {
await caches.open(pe).then((t) => t.put(
fe,
new Response(
JSON.stringify(e),
{ headers: { "Content-Type": "application/json" } }
)
));
}
async function oe(e, t) {
const r = await S(), n = r.find((o) => o.url === e), s = {
...n,
lastModified: Date.now(),
status: t
}, a = [
...r.filter((o) => o.url !== e),
s
];
await X(a).then(() => {
n.status !== t && Je({
type: "mirror-status-changed",
mirror: e,
status: t
});
});
}
async function ze() {
const e = Date.now(), t = await S(), r = t.every((s) => s.status === z);
let n;
r ? n = t.map((s) => ({
...s,
lastModified: e,
status: x
})) : n = t.map((s) => s.lastModified < e - Ye ? {
...s,
lastModified: e,
status: x
} : s), await X(n);
}
async function ie() {
const t = (await S()).filter((r) => r.status === x);
return t.find((r) => r.isPrimary) || t[0];
}
async function E(e, t) {
const r = new URL(e.url), n = r.pathname, s = r.searchParams, a = e.destination === "document", { mirroring: o } = await t.getConfiguration(), i = o.timeout ?? 3e3, c = n.replace(/^\//, "");
let l = [];
a && await ze();
let f = await ie();
for (; f; ) {
const p = f.url, h = new URL(c, p);
h.search = s.toString();
const $ = new AbortController();
try {
const v = await Promise.race([
Xe(i),
fetch(
new Request(h, {
...e,
signal: $.signal
})
)
]), U = new Headers(v.headers);
return U.append("x-mirror", p), U.append("x-time-cached", Date.now()), await oe(p, x), t.logger(`(mirroring) Request successful on mirror: ${p}, URL: ${h}`), new Response(v.body, { ...v, headers: U });
} catch (v) {
$.abort(), l.push(v), await oe(p, z), t.logger(`(mirroring) Request failed on mirror: ${p}, URL: ${h}`);
}
f = await ie();
}
return Promise.reject(
new Error("networking.js (fetchFromMirrors): Out of mirrors.", {
cause: l
})
);
}
function Xe(e) {
return new Promise((t, r) => {
setTimeout(() => {
r(
new Response("", {
status: 408,
statusText: "Request Timeout"
})
);
}, e);
});
}
function tr({ serviceWorkerController: e } = {}) {
if (!e)
throw new Error("You must pass a serviceWorkerController plugin to registerBookmarkApi");
e.modules.push("bookmark");
const t = ({ event: s }) => new RegExp(`/${u}/bookmark/?$`).test(s.request.url), r = ({ event: s }) => ge(u).test(s.request.url), n = new J(`${u}-bookmarks`, {
maxEntries: 100
});
e.logger("(bookmark) Module registered."), e.addRoute(t, Ke, "GET"), e.logger(`(bookmark) GET "/${u}/bookmark/" endpoint registered.`), e.addRoute(r, Qe, "GET"), e.logger(`(bookmark) GET "/${u}/bookmark/:path" endpoint registered.`), e.addRoute(r, (s) => Ze({ ...s, expirationManager: n }), "POST"), e.logger(`(bookmark) POST "/${u}/bookmark/:path" endpoint registered.`), e.addRoute(r, et, "DELETE"), e.logger(`(bookmark) DELETE "/${u}/bookmark/:path" endpoint registered.`), e.addRoute(tt, (s) => rt({ ...s, expirationManager: n }), "GET");
}
async function Ke() {
const e = `${u}-bookmarks`, t = await caches.open(e), n = (await t.keys()).map((s) => t.match(s).then(async (a) => {
const o = await a.text(), i = a.headers.get("X-metadata");
return {
url: s.url,
path: new URL(s.url).pathname,
html: o,
metadata: i ? JSON.parse(i) : null
};
}));
return Promise.all(n).then((s) => new Response(JSON.stringify(s), {
status: 200,
headers: {
"Content-Type": "application/json"
}
})).catch((s) => new Response(null, {
status: 500,
statusText: s.message
}));
}
async function Qe({ event: e }) {
const t = new URL(e.request.url), r = `${u}-bookmarks`, n = await caches.open(r), s = K(u, t);
return s ? n.match(s).then(async (a) => {
if (!a)
return new Response(null, {
status: 404,
statusText: "Not Found"
});
const o = await a.text(), i = a.headers.get("X-metadata"), c = JSON.stringify({
url: e.request.url,
path: s,
html: o,
metadata: i ? JSON.parse(i) : null
});
return new Response(c, {
status: 200,
headers: {
"Content-Type": "application/json"
}
});
}) : new Response(null, {
status: 400,
statusText: "Not a valid bookmark path"
});
}
async function Ze({ event: e, serviceWorkerController: t, expirationManager: r }) {
const n = new URL(e.request.url), s = `${u}-bookmarks`, a = K(u, n);
if (!a)
return new Response(JSON.stringify({}), {
status: 400,
statusText: "Not a valid bookmark path"
});
const o = new Request(a), i = await t.fetcher(o);
if (!i.ok)
return i;
let c;
try {
const { metadata: l } = await e.request.json();
c = new Response(i.body, { ...i }), c.headers.set("x-metadata", JSON.stringify(l));
} catch {
c = i;
}
return caches.open(s).then((l) => l.put(a, c)).then(async () => (await r.updateTimestamp(a), await r.expireEntries(), new Response(null, {
status: 200
}))).catch((l) => new Response(null, {
status: 500,
statusText: l.message
}));
}
async function et({ event: e }) {
const t = new URL(e.request.url), r = `${u}-bookmarks`, n = K(u, t);
return n ? caches.open(r).then((s) => s.delete(n)).then(() => new Response(null, {
status: 200
})).catch((s) => new Response(null, {
status: 500,
statusText: s.message
})) : new Response(JSON.stringify({}), {
status: 400,
statusText: "Not a valid bookmark path"
});
}
function ge(e) {
return new RegExp(`/${e}/bookmark/(.{1,})`);
}
function K(e, t) {
const r = ge(e);
return decodeURIComponent(t.pathname.match(r)[1]);
}
async function tt({ event: e }) {
return caches.open(`${u}-bookmarks`).then((t) => t.match(e.request));
}
async function rt({
serviceWorkerController: e,
expirationManager: t,
event: r
}) {
const s = e.modules.includes("mirroring") ? (a) => E(a, e) : fetch;
return Y({
cacheName: `${u}-bookmarks`,
expirationManager: t,
fetcher: s,
event: r
});
}
const nt = () => ({});
function T(e, t) {
return t ? `${e}?revision=${t}` : e;
}
const rr = (e = {}) => {
const { serviceWorkerController: t } = e;
if (!t)
throw new Error("You must pass a serviceWorkerController plugin to registerContentBundles");
t.modules.push("content-bundles"), t.logger("(content-bundles) Module registered."), t.addInstallHandler(st), t.addActivateHandler(at);
const r = ({ event: n }) => new RegExp(`/${u}/content-bundles/?$`).test(n.request.url);
t.addRoute(r, ot, "GET"), t.logger(`(content-bundles) GET "/${u}/content-bundles/" endpoint registered.`), t.addRoute(it, ct, "GET");
};
async function st({ serviceWorkerController: e }) {
const { contentBundles: t } = await e.getConfiguration();
return t.pages.map((r) => {
const { metadata: n = {} } = r, s = new Request(r.url);
return e.fetcher(s).then((a) => {
if (!a.ok)
throw new Error(`Failed to fetch ${r.url}`);
let o;
try {
o = new Response(a.body, { ...a }), o.headers.set("x-metadata", JSON.stringify(n));
} catch {
o = a;
}
return o;
}).then((a) => {
const o = T(r.url, r.revision);
return caches.open(`${u}-content-bundles`).then((i) => i.put(o, a));
}).catch((a) => (console.info(a.message, ", skipping caching content bundle"), Promise.resolve()));
});
}
async function at({ serviceWorkerController: e }) {
const { contentBundles: t } = await e.getConfiguration(), r = t.pages.map((n) => T(n.url, n.revision));
return caches.open(`${u}-content-bundles`).then((n) => n.keys().then((s) => {
s.forEach((a) => {
const o = a.url.replace(self.location.origin, "");
if (!r.includes(o))
return n.delete(a);
});
}));
}
const ot = async () => {
try {
const e = await caches.open(`${u}-content-bundles`), t = await e.keys(), r = await Promise.all(t.map(async (n) => {
const s = await e.match(n), a = s.headers.get("Content-Type"), o = s.headers.get("X-metadata"), i = await s.text();
return {
url: n.url,
path: new URL(n.url).pathname,
metadata: o ? JSON.parse(o) : null,
contentType: a,
content: i
};
}));
return new Response(JSON.stringify(r), {
status: 200,
headers: {
"Content-Type": "application/json"
}
});
} catch (e) {
return new Response(null, {
status: 500,
statusText: e.message
});
}
};
async function it({ event: e }) {
return caches.open(`${u}-content-bundles`).then((t) => t.match(e.request, { ignoreSearch: !0 }));
}
async function ct({
serviceWorkerController: e,
event: t
}) {
const n = e.modules.includes("mirroring") ? (s) => E(s, e) : fetch;
return Y({
cacheName: `${u}-content-bundles`,
fetcher: n,
event: t
});
}
const ut = (e) => !e || !e.pages ? (console.warn("[content-bundles]: No configuration provided"), { pages: [] }) : {
pages: (Array.isArray(e.pages) ? e.pages : [e.pages]).map((n) => {
const { metadata: s = {} } = n;
return typeof n == "string" ? { url: n, revision: null, metadata: s } : { url: n.url, revision: n.revision || null, metadata: s };
})
}, nr = (e = {}) => {
const { serviceWorkerController: t } = e;
if (!t)
throw new Error("You must pass a serviceWorkerController plugin to registerOfflineAssets");
t.modules.push("offline-assets"), t.logger("(offline-assets) Module registered."), t.addInstallHandler(lt), t.addActivateHandler(dt), t.addRoute(ht, pt, "GET");
};
async function lt({ serviceWorkerController: e }) {
const { offlineAssets: t = {} } = await e.getConfiguration();
return t.assets.map((r) => {
const n = T(r.url, r.revision);
return Promise.all([
caches.open(`${u}-offline-assets`).then((s) => {
const a = new Request(n);
return e.fetcher(a).then((o) => {
if (o.ok)
return s.put(n, o);
});
})
]);
});
}
async function dt({ serviceWorkerController: e }) {
const { offlineAssets: t } = await e.getConfiguration(), r = t.assets.map((n) => T(n.url, n.revision));
return caches.open(`${u}-offline-assets`).then((n) => n.keys().then((s) => {
s.forEach((a) => {
const o = a.url.replace(self.location.origin, "");
if (!r.includes(o))
return n.delete(a);
});
}));
}
async function ht({ event: e }) {
return caches.open(`${u}-offline-assets`).then((t) => t.match(e.request, { ignoreSearch: !0 }));
}
function pt({ event: e, serviceWorkerController: t }) {
return caches.open(`${u}-offline-assets`).then((r) => r.match(e.request, { ignoreSearch: !0 }).then((n) => (n && t.logger(
`(offline-assets) Match found. Returning cached response for URL path: ${e.request.url}`
), n)));
}
const ft = (e) => {
if (!e || !e.assets)
throw new Error("Missing offlineAssets.assets in configuration.");
return {
assets: (Array.isArray(e.assets) ? e.assets : [e.assets]).map((n) => typeof n == "string" ? { url: n, revision: null } : { url: n.url, revision: n.revision || null })
};
}, gt = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360"><path fill="#7A7D95" d="M0 0h640v360H0z"/><g fill="#FFF"><path d="M258.649 118.314v122.223h122.223V118.314H258.649zm119.723 2.5v81.901l-33.228-32.793-25.763 24.932-16.711-16.614-41.521 41.038v-98.464h117.223zM261.149 238.037v-15.245l41.517-41.033 33.458 33.265 1.762-1.773-16.73-16.634 23.972-23.2 33.245 32.81v31.81H261.149z"/><path d="M294.185 165.228c8.089 0 14.67-6.581 14.67-14.669s-6.581-14.669-14.67-14.669-14.669 6.581-14.669 14.669 6.58 14.669 14.669 14.669zm0-26.839c6.71 0 12.17 5.459 12.17 12.169s-5.459 12.169-12.17 12.169-12.169-5.459-12.169-12.169 5.458-12.169 12.169-12.169z"/></g></svg>
`, sr = (e = {}) => {
const { serviceWorkerController: t } = e;
if (!t)
throw new Error(
"You must pass a serviceWorkerController plugin to registerFallbackImage"
);
t.modules.push("fallback-image"), t.logger("(fallback-image) Module registered."), t.addFallbackHandler(
({ event: r }) => r.request.destination === "image",
({ event: r, serviceWorkerController: n }) => (n.logger(`(fallback-image) Returning fallback image for URL: ${r.request.url}`), new Response(gt, {
headers: { "Content-Type": "image/svg+xml" }
}))
);
}, mt = () => ({});
function bt({
language: e,
textDirection: t,
templateStrings: r,
mainCSS: n,
mainJS: s
}) {
return {
// Core
"{{ language }}": e,
"{{ textDirection }}": t,
"{{ mainCSS }}": n,
"{{ mainJS }}": s,
// Template strings
"{{ pageTitle }}": (r == null ? void 0 : r.pageTitle) || "451 Tools",
"{{ pageDescription }}": (r == null ? void 0 : r.pageDescription) || "Censorship Resilient and Distributed Publishing",
"{{ homeLinkText }}": (r == null ? void 0 : r.homeLinkText) || "Home",
"{{ bookmarksTabTitle }}": (r == null ? void 0 : r.bookmarksTabTitle) || "Bookmarks",
"{{ editorialTabTitle }}": (r == null ? void 0 : r.editorialTabTitle) || "Editorial",
"{{ aboutTabTitle }}": (r == null ? void 0 : r.aboutTabTitle) || "About",
"{{ copyright }}": (r == null ? void 0 : r.copyright) || `Copyright © ${(/* @__PURE__ */ new Date()).getFullYear()}, Zamaneh Media. All rights reserved.`,
"{{ aboutInnerHTML }}": (r == null ? void 0 : r.aboutInnerHTML) || `
<p>You are on the 451 Tools offline dashboard. This could be because you are experiencing an internet shutdown or your device is not connected to the internet. 451 Tools keeps information available even when the website is censored or when the internet is down.</p>
<p>451 Tools helps publishers maintain reach and readership under the most adversarial circumstances. Find out more at <a href="https://451.tools/" target="_blank">https://451.tools/</a>.</p>
`
};
}
const wt = '<!doctypehtml><html lang="{{ language }}"dir="{{ textDirection }}"><meta charset="UTF-8"><meta name="viewport"content="width=device-width,initial-scale=1"><meta name="description"content="{{ pageDescription }}"><title>{{ pageTitle }}</title><style>{{ mainCSS }}</style><div class="container"><header class="header"><a class="home-link"href="/"><svg xmlns="http://www.w3.org/2000/svg"fill="currentColor"viewBox="0 0 16 16"width="28"height="28"><path d="M8 1.32.66 8.133l.68.734.66-.613V14h5V9h2v5h5V8.254l.66.613.68-.734Zm0 1.36 5 4.648V13h-3V8H6v5H3V7.328Z"/></svg> <span class="link-text">{{ homeLinkText }}</span></a><h1 class="title">{{ pageTitle }}</h1><p class="description">{{ pageDescription }}</p><svg class="offline-icon"xmlns="http://www.w3.org/2000/svg"viewBox="0 0 500 500"aria-hidden="true"><path d="M355.5 105.5c11.4 36.1 17.7 73.6 18.7 111.4 4.3.5 8.5 1.1 12.6 2h69.8c-3.2-52.6-25.4-102.2-62.1-139.9-12.2 10-25.3 18.9-39 26.5M99 405.2c.7.7 1.5 1.3 2.1 1.8 2.1 1.6 4.4 3.1 6.5 4.7 3.2 2.3 6.5 4.5 9.7 6.6 2.3 1.5 4.7 2.8 6.9 4.2 3.4 1.9 6.6 3.9 10 5.6 2.4 1.3 4.9 2.5 7.4 3.7 3.2 1.6 6.8 3.1 10 4.7 2.4 1 4.7 2 7.1 2.8-12.9-16.2-23.6-34.1-31.5-53.4-10 5.7-19.3 12.1-28.2 19.3m171.2-158.9h-19.9v19.2c6-7 12.7-13.4 19.9-19.2m175.6.2c3.6 2.8 7 5.8 10.2 9 .3-3 .6-6 .8-9h-11M250.3 13.8v94.1c24.3-1.5 48-6.9 70.4-16.3-17.6-41.8-42.6-70.5-70.4-77.8m123.9 46.4-2.1-1.8c-2.1-1.6-4.4-3.1-6.5-4.7-3.2-2.3-6.5-4.5-9.7-6.6-2.3-1.5-4.7-2.7-6.9-4.2-3.4-1.9-6.6-3.9-10-5.7-2.5-1.3-4.9-2.5-7.4-3.7-3.2-1.6-6.8-3.2-10-4.7-2.4-1-4.7-1.9-7.1-2.7 13 16.2 23.6 34.1 31.5 53.4 9.9-5.7 19.3-12.2 28.2-19.3M250.3 218.8h78.8c5.7-1.2 11.5-2 17.4-2.4-1.2-33.5-6.6-66.7-16.4-98.8-25.4 10.3-52.4 16.3-79.7 17.8l-.1 83.4m-27.5 232.6v-51.7c-4.1-13.1-6.3-26.7-6.4-40.4V358c-22 2-43.5 7.2-63.9 15.7 17.6 41.6 42.5 70.4 70.3 77.7m-105.1-91.5c-11.6-36.7-18-74.9-18.7-113.4H16.6c3.2 52.6 25.4 102.2 62.1 139.9 12.2-10 25.3-18.9 39-26.5M16.3 218.8h82.5c1-38.5 7.1-76.7 18.7-113.4-13.8-7.6-26.8-16.5-39-26.5-36.7 37.7-58.6 87.4-62.2 139.9m235.3 232.1c-.4-.5-.8-1-1.3-1.5v2c.5-.1.9-.3 1.4-.4 0-.1-.1-.1-.1-.1m-125-204.4c1 34.3 6.5 68.4 16.5 101.3 24.2-9.9 49.9-15.7 75.9-17.5.2-1.2.4-2.4.7-3.5.9-3.9 1.9-7.8 3.1-11.6v-68.7h-96.2M159 26.2c-2.4.8-4.7 1.8-7.1 2.7-3.5 1.5-6.9 2.9-10.3 4.5-2.5 1.2-5 2.4-7.4 3.7-3.4 1.6-6.6 3.6-9.9 5.5-2.4 1.5-4.9 2.9-7.1 4.4-3.3 2.1-6.4 4.3-9.6 6.6-2.1 1.6-4.4 3.2-6.5 4.9-.6.5-1.3 1.1-1.9 1.8 8.9 7.3 18.4 13.6 28.3 19.2 7.8-19.1 18.5-37.1 31.5-53.3m-6.5 65.5c22.4 9.2 46.2 14.7 70.3 16.2v-94c-27.8 7.3-52.7 36.3-70.3 77.8m70.3 43.7c-27.5-1.5-54.3-7.4-79.7-17.8-10 32.8-15.5 66.9-16.5 101.2h96.2v-83.4m246.4 163.3c-6.1-11.7-14-22.4-23.3-31.7-10.7-10.7-23.3-19.4-37.1-25.7l-2.1-.9c-15.7-6.8-33-10.6-51.2-10.6-18.1 0-35.3 3.7-50.9 10.5-37.3 16.1-65.4 49.4-74.3 89.9-2.1 9.7-3.2 19.6-3 29.5.2 13.2 2.4 26.4 6.6 39 5.4 16.2 14 31.2 25.3 44 12.7 14.5 28.5 25.9 46.3 33.5 15.4 6.5 32.3 10.1 50.1 10.1 70.8 0 128.2-57.4 128.2-128.2-.1-21.5-5.3-41.6-14.6-59.4zm-43.9 107-22.1 22.1-47.7-47.7-47.7 47.7-22.1-22.1 47.7-47.7-47.7-47.7 22.1-22.1 47.7 47.7 47.7-47.7 22.1 22.1-47.7 47.7 47.7 47.7z"/></svg></header><main class="main-content"><div class="tabs"role="tablist"><button aria-controls="panel-1"aria-selected="false"class="tab tab__hidden"id="tab-1"role="tab"tabindex="-1"disabled="disabled">{{ bookmarksTabTitle }}</button> <button aria-controls="panel-2"aria-selected="false"class="tab tab__hidden"id="tab-2"role="tab"tabindex="-1"disabled="disabled">{{ editorialTabTitle }}</button> <button aria-controls="panel-3"aria-selected="false"class="tab"id="tab-3"role="tab"tabindex="-1">{{ aboutTabTitle }}</button></div><section aria-labelledby="tab-1"class="tab-content"data-bookmarks hidden id="panel-1"role="tabpanel"tabindex="0"></section><section aria-labelledby="tab-2"class="tab-content"data-editorial hidden id="panel-2"role="tabpanel"tabindex="0"></section><section aria-labelledby="tab-3"class="tab-content rich-text"hidden id="panel-3"role="tabpanel"tabindex="0">{{ aboutInnerHTML }}</section></main><footer><p class="copyright">{{ copyright }}</footer></div><script>{{ mainJS }}<\/script>', ce = '*,::after,::before{box-sizing:border-box}*{margin:0}body,html{height:100%}body{-webkit-font-smoothing:antialiased;background-color:var(--background-color,#f9fbfc);color:var(--text-color,#2d2d3b);font-family:system-ui,sans-serif;line-height:1.4}.container{display:flex;flex-direction:column;height:100%;margin:0 auto;max-width:var(--container-width,1200px);padding:0 2rem;width:100%}.header{margin:4rem 0 2.5rem;position:relative}.home-link{display:flex;align-items:center;gap:.5rem;color:var(--primary-color,#1993f6);margin-block-end:1rem;text-decoration:none}.home-link:hover .link-text{text-decoration:underline}.home-link svg{width:28px;height:28px}.title{font-size:2.75rem;line-height:1;margin-block-end:1.5rem}@media screen and (min-width:960px){.title{font-size:5rem}}.description{font-size:1.25rem;max-width:40rem}.offline-icon{display:none;fill:var(--icon-color,#e6e8ec);height:100%;position:absolute;inset-inline-end:0;inset-block-start:0;z-index:-1}@media screen and (min-width:960px){.offline-icon{display:initial}}.main-content{flex-grow:1}.tabs{display:flex;flex-direction:row;flex-wrap:wrap;list-style:none;margin-block-end:2rem;padding:0}@media screen and (min-width:485px){.tabs{border-block-end:1px solid var(--border-color,#e6e8ec);flex-wrap:nowrap;overflow:auto}}.tab{appearance:none;background:0 0;border:none;border-block-end:2px solid transparent;box-shadow:0 1px 0 0 var(--border-color,#e6e8ec);color:var(--tab-text-color,#7a7d95);cursor:pointer;font:inherit;font-size:1.25rem;font-weight:700;padding:.5rem 1rem;transition:border-color .2s ease-in-out;width:50%}@media screen and (min-width:485px){.tab{box-shadow:none;width:initial}}.tab__hidden{display:none}.tab:disabled{cursor:not-allowed;opacity:.5;order:1}.tab:disabled::before{content:"🔒";margin-inline-end:.25rem}.tab[aria-selected=true]{border-color:var(--primary-color,#1993f6);color:var(--primary-color,#1993f6)}.tab:hover:not(:disabled){border-color:var(--primary-color,#1993f6)}.rich-text{max-width:40rem}.rich-text>*+*{margin-block-start:1.5rem}.rich-text h2,.rich-text h3{line-height:1.25}.rich-text h2{font-size:2.25rem}@media screen and (min-width:960px){.rich-text h2{font-size:2.75rem}}.rich-text h3{font-size:1.75rem}@media screen and (min-width:960px){.rich-text h3{font-size:2.25rem}}.rich-text a{color:var(--primary-color,#1993f6)}.rich-text blockquote{border-inline-start:0.25rem solid var(--primary-color,#1993f6);padding-inline-start:1rem}.rich-text cite,.rich-text figcaption{display:block;font-style:italic;margin-block-start:0.5rem}.rich-text figcaption{text-align:center}.rich-text img{display:block;margin-inline-start:auto;margin-inline-end:auto;max-width:100%}.rich-text ol,.rich-text ul{list-style-position:inside;padding:0}.bookmarks,.content-bundles{display:flex;flex-wrap:wrap;gap:1.5rem;list-style:none;padding:0}.card{background-color:var(--card-background-color,#fff);border-radius:.5rem;border:1px solid var(--border-color,#e6e8ec);box-shadow:var(--card-box-shadow,0 8px 24px rgba(149,157,165,.2));width:100%}@media screen and (min-width:960px){.card{width:calc(50% - .75rem)}}.card-link{border-radius:.5rem;color:inherit;display:flex;height:100%;overflow:hidden;text-decoration:none;transition:box-shadow .2s ease-in-out}.card-link:hover{box-shadow:0 0 0 2px var(--primary-color,#1993f6)}.card-image{flex-shrink:0;height:100%;min-height:100px;object-fit:cover;width:100px}.card-image[src=""]{display:none}.card-content{padding:1rem}.card-title{font-size:1.25rem}.card-description,.card-title{-webkit-box-orient:vertical;-webkit-line-clamp:2;display:-webkit-box;overflow-wrap:break-word;overflow:hidden;word-break:break-word}.card-description:not(:empty){-webkit-line-clamp:3;margin-block-start:0.5rem}.copyright{margin:2rem 0;text-align:center}', yt = `(async function(){const bookmarksPlaceholder=document.querySelector("[data-bookmarks]");const serviceWorkerRegistration=await navigator.serviceWorker.getRegistration();if(!serviceWorkerRegistration){return}let configuration;try{configuration=await caches.open("configuration").then((cache=>cache.match("/configuration.json"))).then((res=>res.json()))}catch{configuration={}}if(configuration.bookmark){const bookmarks=await fetch("/451-tools/bookmark/").then((res=>res.json())).catch((()=>[]));if(bookmarks.length<1){renderNoBookmarks()}else{const bookmarksList=document.createElement("ul");const bookmarksHtml=bookmarks.map(generateCardHtml).join("");bookmarksList.innerHTML=bookmarksHtml;bookmarksList.classList.add("bookmarks");bookmarksPlaceholder.appendChild(bookmarksList)}const bookmarksTab=document.querySelector("#tab-1");bookmarksTab.classList.remove("tab__hidden");bookmarksTab.removeAttribute("disabled")}else if(configuration.fallbackPages.showHiddenTabs){const bookmarksTab=document.querySelector("#tab-1");bookmarksTab.classList.remove("tab__hidden")}function renderNoBookmarks(){const noBookmarksMessage=document.createElement("p");noBookmarksMessage.innerText="You have no bookmarks.";bookmarksPlaceholder.appendChild(noBookmarksMessage)}const contentBundlesPlaceholder=document.querySelector("[data-editorial]");const isContentBundlesUsed=await caches.has("451-tools-content-bundles");if(isContentBundlesUsed){const contentBundles=await caches.open("451-tools-content-bundles").then((cache=>cache.keys())).then((requests=>Promise.all(requests.map((async request=>{const response=await caches.match(request);const html=await response.text();return{path:new URL(request.url).pathname,url:request.url.split("?")[0],html:html}})))));if(contentBundles.length<1){renderNoContentBundles()}else{const contentBundlesList=document.createElement("ul");const contentBundlesHtml=contentBundles.reverse().map(generateCardHtml).join("");contentBundlesList.innerHTML=contentBundlesHtml;contentBundlesList.classList.add("content-bundles");contentBundlesPlaceholder.appendChild(contentBundlesList)}const contentBundlesTab=document.querySelector("#tab-2");contentBundlesTab.classList.remove("tab__hidden");contentBundlesTab.removeAttribute("disabled")}else if(configuration.fallbackPages.showHiddenTabs){const contentBundlesTab=document.querySelector("#tab-2");contentBundlesTab.classList.remove("tab__hidden")}function renderNoContentBundles(){const noContentBundlesMessage=document.createElement("p");noContentBundlesMessage.innerText="You have no content bundles.";contentBundlesPlaceholder.appendChild(noContentBundlesMessage)}function generateCardHtml(response){const cardItem=document.createElement("li");const cardLink=document.createElement("a");const cardImage=document.createElement("img");const cardContent=document.createElement("div");const cardTitle=document.createElement("h2");const cardDescription=document.createElement("p");const html=document.createElement("html");html.innerHTML=response.html;const title=response.metadata?.title||html.querySelector("title")?.innerText||response.url;const description=response.metadata?.description||html.querySelector('meta[name="description"]')?.content||"";const imageSrc=response.metadata?.image?.src||html.querySelector('meta[property="og:image"]