diff options
| author | verdant <im@verdant.ee> | 2026-05-31 16:33:36 +0800 |
|---|---|---|
| committer | verdant <im@verdant.ee> | 2026-05-31 16:33:36 +0800 |
| commit | 172adfb014596423d7cc3043f2a288ac01884cba (patch) | |
| tree | f788b0b10f330daf8c3a6d1a8ed656a1d1253390 /static/js/webmention.js | |
| parent | f9869a2cf060976c69aeade3d3f231bf1fbdddd3 (diff) | |
| download | blog-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.js | 478 |
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) => - ({ - "&": "&", - "<": "<", - ">": ">", - '"': """, - })[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] += "…"; - 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 |
