aboutsummaryrefslogtreecommitdiffstats
path: root/static/js/webmention.js
diff options
context:
space:
mode:
authorverdant <im@verdant.ee>2026-05-31 16:33:36 +0800
committerverdant <im@verdant.ee>2026-05-31 16:33:36 +0800
commit172adfb014596423d7cc3043f2a288ac01884cba (patch)
treef788b0b10f330daf8c3a6d1a8ed656a1d1253390 /static/js/webmention.js
parentf9869a2cf060976c69aeade3d3f231bf1fbdddd3 (diff)
downloadblog-172adfb014596423d7cc3043f2a288ac01884cba.tar.gz
blog-172adfb014596423d7cc3043f2a288ac01884cba.zip
Add minify configuration, modify favicon url
Diffstat (limited to 'static/js/webmention.js')
-rw-r--r--static/js/webmention.js478
1 files changed, 0 insertions, 478 deletions
diff --git a/static/js/webmention.js b/static/js/webmention.js
deleted file mode 100644
index d8cada8..0000000
--- a/static/js/webmention.js
+++ /dev/null
@@ -1,478 +0,0 @@
-/* webmention.js
-
-Simple thing for embedding webmentions from webmention.io into a page, client-side.
-
-(c)2018-2022 fluffy (http://beesbuzz.biz)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-GitHub repo (for latest released versions, issue tracking, etc.):
-
- https://github.com/PlaidWeb/webmention.js
-
-Basic usage:
-
-<script src="/path/to/webmention.js" data-param="val" ... async />
-<div id="webmentions"></div>
-
-Allowed parameters:
-
- page-url:
-
- The base URL to use for this page. Defaults to window.location
-
- add-urls:
-
- Additional URLs to check, separated by |s
-
- id:
-
- The HTML ID for the object to fill in with the webmention data.
- Defaults to "webmentions"
-
- wordcount:
-
- The maximum number of words to render in reply mentions.
-
- max-webmentions:
-
- The maximum number of mentions to retrieve. Defaults to 30.
-
- prevent-spoofing:
-
- By default, Webmentions render using the mf2 'url' element, which plays
- nicely with webmention bridges (such as brid.gy and telegraph)
- but allows certain spoofing attacks. If you would like to prevent
- spoofing, set this to a non-empty string (e.g. "true").
-
- sort-by:
-
- What to order the responses by; defaults to 'published'. See
- https://github.com/aaronpk/webmention.io#api
-
- sort-dir:
-
- The order to sort the responses by; defaults to 'up' (i.e. oldest
- first). See https://github.com/aaronpk/webmention.io#api
-
- comments-are-reactions:
-
- If set to a non-empty string (e.g. "true"), will display comment-type responses
- (replies/mentions/etc.) as being part of the reactions
- (favorites/bookmarks/etc.) instead of in a separate comment list.
-
-A more detailed example:
-
-<!-- If you want to translate the UI -->
-<script src="/path/to/umd/i18next.js"></script>
-<script>
- // Setup i18next as described in https://www.i18next.com/overview/getting-started#basic-sample
-</script>
-<!-- Otherwise, only using the following is fine -->
-<script src="/path/to/webmention.min.js"
- data-id="webmentionContainer"
- data-wordcount="30"
- data-prevent-spoofing="true"
- data-comments-are-reactions="true"
- />
-
-*/
-
-// Begin LibreJS code licensing
-// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt
-
-(function () {
- "use strict";
-
- // Shim i18next
- window.i18next = window.i18next || {
- t: function t(/** @type {string} */ key) {
- return key;
- },
- };
- const t = window.i18next.t.bind(window.i18next);
-
- /**
- * Read the configuration value.
- *
- * @param {string} key The configuration key.
- * @param {string} dfl The default value.
- * @returns {string}
- */
- function getCfg(key, dfl) {
- return document.currentScript.getAttribute("data-" + key) || dfl;
- }
-
- const refurl = getCfg("page-url", window.location.href.replace(/#.*$/, ""));
- const addurls = getCfg("add-urls", undefined);
- const containerID = getCfg("id", "webmentions");
- /** @type {Number} */
- const textMaxWords = getCfg("wordcount");
- const maxWebmentions = getCfg("max-webmentions", 30);
- const mentionSource = getCfg("prevent-spoofing") ? "wm-source" : "url";
- const sortBy = getCfg("sort-by", "published");
- const sortDir = getCfg("sort-dir", "up");
- /** @type {boolean} */
- const commentsAreReactions = getCfg("comments-are-reactions", false);
-
- /**
- * @typedef MentionType
- * @type {"in-reply-to"|"like-of"|"repost-of"|"bookmark-of"|"mention-of"|"rsvp"|"follow-of"}
- */
-
- /**
- * Maps a reaction to a hover title.
- *
- * @type {Record<MentionType, string>}
- */
- const reactTitle = {
- "in-reply-to": t("replied"),
- "like-of": t("liked"),
- "repost-of": t("reposted"),
- "bookmark-of": t("bookmarked"),
- "mention-of": t("mentioned"),
- rsvp: t("RSVPed"),
- "follow-of": t("followed"),
- };
-
- /**
- * Maps a reaction to an emoji.
- *
- * @type {Record<MentionType, string>}
- */
- const reactEmoji = {
- "in-reply-to": "💬",
- "like-of": "❤️",
- "repost-of": "🔄",
- "bookmark-of": "⭐️",
- "mention-of": "💬",
- rsvp: "📅",
- "follow-of": "🐜",
- };
-
- /**
- * @typedef RSVPEmoji
- * @type {"yes"|"no"|"interested"|"maybe"|null}
- */
-
- /**
- * Maps a RSVP to an emoji.
- *
- * @type {Record<RSVPEmoji, string>}
- */
- const rsvpEmoji = {
- yes: "✅",
- no: "❌",
- interested: "💡",
- maybe: "💭",
- };
-
- /**
- * HTML escapes the string.
- *
- * @param {string} text The string to be escaped.
- * @returns {string}
- */
- function entities(text) {
- return text.replace(
- /[&<>"]/g,
- (tag) =>
- ({
- "&": "&amp;",
- "<": "&lt;",
- ">": "&gt;",
- '"': "&quot;",
- })[tag] || tag,
- );
- }
-
- /**
- * Creates the markup for an reaction image.
- *
- * @param {Reaction} r
- * @param {boolean} isComment
- * @returns {string}
- */
- function reactImage(r, isComment) {
- const who = entities(r.author?.name || r.url.split("/")[2]);
- /** @type {string} */
- let response = reactTitle[r["wm-property"]] || t("reacted");
- if (!isComment && r.content && r.content.text) {
- response += ": " + extractComment(r);
- }
-
- let authorPhoto = "";
- if (r.author && r.author.photo) {
- authorPhoto = `
- <img
- src="${entities(r.author.photo)}"
- loading="lazy"
- decoding="async"
- alt="${who}"
- >
- `;
- } else {
- authorPhoto = `
- <img
- class="missing"
- src="data:image/webp;base64,UklGRkoCAABXRUJQVlA4TD4CAAAvP8APAIV0WduUOLr/m/iqY6SokDJSMD5xYX23SQizRsVdZmIj/f6goYUbiOj/BED7MOPReuBNT3vBesSzIex+SeqMFFkjebFmzH3S7POxDSJ1yaCbCmMnS2R46cRMPyQLw4GBK4esdK60pYwsZakecUCl5zsHv/5cPH08nx9/7i6rEEVCg2hR8VSd30PxMZpVoJZQO6Dixgg6X5oKFCmlVHIDmmMFShWumAXgCuyqVN8hHff/k+9fj8+ei7BVjpxBmZCUJv+6DhWGZwWvs+UoLHFCKsPYpfJtIcEXBTopEEsKwedZUv4ku1FZErKULLyQwFGgnmTs2vBD5qu44xwnG9uyjgrFOd+KRVlXyQfwQlauydaU6AVI7OjKXLUEqNtxJBmQegNDZgV7lxxqYMOMrDyC1NdAGbdiH9Ij0skjG+oTyfO0lmjdgvoH8iIgreuBMRYLSH+R3sAztXgL+XfS7E2bmfo6gnS0TrpnzHT7kL+skj7PgHuBwv/zpN8LDLQg7zfJZLBubMKnyeh6ZGyfDEfc2LYpnlUtG7JqsSHq1WoASbUS4KVaLwB8be5mfsGMDwBcm5VxbuxWxx3nkFanB6lYqsqSkOGkKicoDvXsneR7BkKU7DtaEuT7+pxBGVwx+9gVyqf2pVA9sC2CsmjZ1RJqEJHS4Tj/pCcS0JoyBYOsB91Xjh3OFfQPQhvCAYyeLJlaOoFp0XNNuD0BC8exr8uPx7D1JWkwFdZIXmD3MOPReuDNzHjBesSzIbQD"
- alt="${who}$"
- >
- `;
- }
-
- let rsvp = "";
- if (r.rsvp && rsvpEmoji[r.rsvp]) {
- rsvp = `<sub>${rsvpEmoji[r.rsvp]}</sub>`;
- }
-
- return `
- <a
- class="reaction"
- rel="nofollow ugc"
- title="${who} ${response}"
- href="${r[mentionSource]}"
- >
- ${authorPhoto}
- ${reactEmoji[r["wm-property"]] || "💥"}
- ${rsvp}
- </a>
- `;
- }
-
- /**
- * Strip the protocol off a URL.
- *
- * @param {string} url The URL to strip protocol off.
- * @returns {string}
- */
- function stripurl(url) {
- return url.substr(url.indexOf("//"));
- }
-
- /**
- * Deduplicate multiple mentions from the same source URL.
- *
- * @param {Array<Reaction>} mentions Mentions of the source URL.
- * @return {Array<Reaction>}
- */
- function dedupe(mentions) {
- /** @type {Array<Reaction>} */
- const filtered = [];
- /** @type {Record<string, boolean>} */
- const seen = {};
-
- mentions.forEach(function (r) {
- // Strip off the protocol (i.e. treat http and https the same)
- const source = stripurl(r.url);
- if (!seen[source]) {
- filtered.push(r);
- seen[source] = true;
- }
- });
-
- return filtered;
- }
-
- /**
- * Extract comments from a reaction.
- *
- * @param {Reactions} c
- * @returns string
- */
- function extractComment(c) {
- let text = entities(c.content.text);
-
- if (textMaxWords) {
- let words = text.replace(/\s+/g, " ").split(" ", textMaxWords + 1);
- if (words.length > textMaxWords) {
- words[textMaxWords - 1] += "&hellip;";
- words = words.slice(0, textMaxWords);
- text = words.join(" ");
- }
- }
-
- return text;
- }
-
- /**
- * Format comments as HTML.
- *
- * @param {Array<Reaction>} comments The comments to format.
- * @returns string
- */
- function formatComments(comments) {
- const headline = `<h2>${t("Responses")}</h2>`;
- const markup = comments
- .map((c) => {
- const image = reactImage(c, true);
-
- let source = entities(c.url.split("/")[2]);
- if (c.author && c.author.name) {
- source = entities(c.author.name);
- }
- const link = `<a class="source" rel="nofollow ugc" href="${c[mentionSource]}">${source}</a>`;
-
- let linkclass = "name";
- let linktext = `(${t("mention")})`;
- if (c.name) {
- linkclass = "name";
- linktext = entities(c.name);
- } else if (c.content && c.content.text) {
- linkclass = "text";
- linktext = extractComment(c);
- }
-
- const type = `<span class="${linkclass}">${linktext}</span>`;
-
- return `<li>${image} ${link} ${type}</li>`;
- })
- .join("");
- return `
- ${headline}
- <ul class="comments">${markup}</ul>
- `;
- }
-
- /**
- * @typedef {Object} Reaction
- * @property {string} url
- * @property {Object?} author
- * @property {string?} author.name
- * @property {string?} author.photo
- * @property {Object?} content
- * @property {string?} content.text
- * @property {RSVPEmoji?} rsvp
- * @property {MentionType?} wm-property
- * @property {string?} wm-source
- */
-
- /**
- * Formats a list of reactions as HTML.
- *
- * @param {Array<Reaction>} reacts List of reactions to format
- * @returns string
- */
- function formatReactions(reacts) {
- const headline = `<h2>${t("Reactions")}</h2>`;
-
- const markup = reacts.map((r) => reactImage(r)).join("");
-
- return `
- ${headline}
- <ul class="reacts">${markup}</ul>
- `;
- }
-
- /**
- * @typedef WebmentionResponse
- * @type {Object}
- * @property {Array<Reaction>} children
- */
-
- /**
- * Register event listener.
- */
- window.addEventListener("load", async function () {
- const container = document.getElementById(containerID);
- if (!container) {
- // no container, so do nothing
- return;
- }
-
- const pages = [stripurl(refurl)];
- if (!!addurls) {
- addurls.split("|").forEach(function (url) {
- pages.push(stripurl(url));
- });
- }
-
- let apiURL = `https://webmention.io/api/mentions.jf2?per-page=${maxWebmentions}&sort-by=${sortBy}&sort-dir=${sortDir}`;
-
- pages.forEach(function (path) {
- apiURL += `&target[]=${encodeURIComponent("http:" + path)}&target[]=${encodeURIComponent("https:" + path)}`;
- });
-
- /** @type {WebmentionResponse} */
- let json = {};
- try {
- const response = await window.fetch(apiURL);
- if (response.status >= 200 && response.status < 300) {
- json = await response.json();
- } else {
- console.error("Could not parse response");
- new Error(response.statusText);
- }
- } catch (error) {
- // Purposefully not escalate further, i.e. no UI update
- console.error("Request failed", error);
- }
-
- /** @type {Array<Reaction>} */
- let comments = [];
- /** @type {Array<Reaction>} */
- const collects = [];
-
- if (commentsAreReactions) {
- comments = collects;
- }
-
- /** @type {Record<MentionType, Array<Reaction>>} */
- const mapping = {
- "in-reply-to": comments,
- "like-of": collects,
- "repost-of": collects,
- "bookmark-of": collects,
- "follow-of": collects,
- "mention-of": comments,
- rsvp: comments,
- };
-
- json.children.forEach(function (child) {
- // Map each mention into its respective container
- const store = mapping[child["wm-property"]];
- if (store) {
- store.push(child);
- }
- });
-
- // format the comment-type things
- let formattedComments = "";
- if (comments.length > 0 && comments !== collects) {
- formattedComments = formatComments(dedupe(comments));
- }
-
- // format the other reactions
- let reactions = "";
- if (collects.length > 0) {
- reactions = formatReactions(dedupe(collects));
- }
-
- let finalHtml = `${formattedComments}${reactions}`;
- if (!finalHtml.trim()) {
- finalHtml = '<p class="wm-empty">Nothing.</p>';
- }
-
- // 关键修正:将 finalHtml 变量的内容,赋值给容器的 innerHTML 属性
- container.innerHTML = finalHtml;
- });
-})();
-
-// End-of-file marker for LibreJS
-// @license-end