summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content/about.md31
-rw-r--r--content/categories.md9
-rw-r--r--content/readings.md2
-rw-r--r--layouts/404.html2
-rw-r--r--layouts/partials/style.html180
-rw-r--r--static/js/webmention.js649
6 files changed, 512 insertions, 361 deletions
diff --git a/content/about.md b/content/about.md
index f5214f8..92d9863 100644
--- a/content/about.md
+++ b/content/about.md
@@ -33,27 +33,30 @@ type: no-comments
- 所有文章内容使用[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)协议授权。
- 目前文章字数:{{< word-count "posts" >}}
-## 这个博客是如何搭建的
+## 订阅
-本博客使用快速、易用的静态博客生成程序 [Hugo](https://gohugo.io/),在 [hugo-bearneo](https://github.com/rokcso/hugo-bearneo) 主题的基础上进行修改,静态文件托管在Github Pages,图床使用Cloudflare R2对象存储。我使用 Emacs 撰写文章,并推送到远程仓库构建部署。
+如果你喜欢我的文字,可以用以下链接订阅:
-## 在哪找到我
+- [订阅全部文章](https://www.glowisle.me/atom.xml/)
-- Email: [i@glowisle.me](mailto:i@glowisle.me/)
-- 即时通讯: [Verdant](https://t.me/VerdantIO/)(Telegram)
-- Mastodon: [c7.io/@Verdant](https://c7.io/@Verdant/)
-- Github: [Verdant](https://github.com/yingyu5658/)
+- [订阅周刊](https://www.glowisle.me/categories/回声周刊/atom.xml)
-## 订阅
+- [订阅Telegram频道的消息](https://channel.glowisle.me/rss.xml/)
-如果你喜欢我的文字,可以用以下链接订阅:
+订阅某个分类:
```
-https://www.glowisle.me/atom.xml/
+https://www.glowisle.me/categories/分类名/atom.xml
```
-订阅Telegram频道的消息:
+## 在哪找到我
-```
-https://channel.glowisle.me/rss.xml/
-```
+- Email: [i@glowisle.me](mailto:i@glowisle.me/)
+- 即时通讯: [Verdant](https://t.me/VerdantIO/)(Telegram)
+- Mastodon: [c7.io/@Verdant](https://c7.io/@Verdant/)
+- Github: [Verdant](https://github.com/yingyu5658/)
+
+## 鸣谢
+
+- 本站样式基于 [Hugo Bear Neo](https://github.com/rokcso/hugo-bearneo/) 主题修改
+- 音乐卡片的修改自 [極客死亡計劃](https://github.com/BigCoke233/geek-death-project)
diff --git a/content/categories.md b/content/categories.md
index cdb715b..f8875ad 100644
--- a/content/categories.md
+++ b/content/categories.md
@@ -20,8 +20,15 @@ title: 分类
技术实践 | 技术观点
+### [📖 回声周刊](/categories/回声周刊)
+
+每周优质阅读分享 | 近期发生的事
+
---
+<!--
+
##### [~~🧻 往昔~~](/categories/往昔)
-###### ~~早期不成熟的文章合集 | 包含偏激观点 | 冒犯性用语 | 不建议阅读~~
+###### ~~早期不成熟的文章合集 | 包含偏激观点 | 冒犯性用语 | 不建议阅读~~
+-->
diff --git a/content/readings.md b/content/readings.md
index 18b710f..6710650 100644
--- a/content/readings.md
+++ b/content/readings.md
@@ -3,7 +3,7 @@ date: "2025-06-07T13:08:07+08:00"
title: 读书
type: no-comments
---
-这里记录着我读过的书,有时候写写书评或读后感。
+这里记录着我读过的书,有时候写写书评或读后感,你可以在[读书](/categories/读书/)页面查看所有与书籍有关的文章。
- 「⭐」表示我很喜欢的书籍
- 「⚪」表示我觉得还可以的书籍
diff --git a/layouts/404.html b/layouts/404.html
index 15198aa..8484782 100644
--- a/layouts/404.html
+++ b/layouts/404.html
@@ -1,4 +1,4 @@
{{ define "title" }}404{{ end }} {{ define "main" }}
<h1>404</h1>
-<h2>ʕノ•ᴥ•ʔノ ︵ ┻━┻</h2>
+<h2>你似乎来到了无人的岛屿。</h2>
{{ end }}
diff --git a/layouts/partials/style.html b/layouts/partials/style.html
index f108da6..8828dbd 100644
--- a/layouts/partials/style.html
+++ b/layouts/partials/style.html
@@ -4,7 +4,7 @@
--width-max: 720px;
--font-primary: "Noto Serif SC", "Source Han Serif SC", serif;
--font-secondary: monospace;
- --font-size-primary: 1em;
+ --font-size-primary: 1.105em;
--font-size-secondary: 0.8em;
--body-bg-color: #ffffff;
--bold-text-color: #222;
@@ -22,6 +22,18 @@
--caption-text-color: #666;
--toc-text-color: #e5e5e5;
--toc-hover-color: #655e5e;
+
+ --music-bg-hover-light: #e5e5e5; /* neutral-200 */
+ --music-bg-light: rgba(229, 229, 229, 0.5); /* neutral-200/50 */
+ --music-text-secondary: #666;
+ --music-shadow:
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ }
+
+ .post-title a:hover,
+ a.item-link:hover,
+ .blog-posts a:hover {
+ text-decoration: underline 0.5px !important;
}
.wm {
@@ -37,6 +49,10 @@
margin-top: 1px;
}
+ article p {
+ line-height: 40px;
+ }
+
.item-link {
white-space: nowrap;
overflow: hidden;
@@ -83,18 +99,147 @@
content: "/";
}
- .site-name:hover {
- background-color: transparent;
- text-decoration: none;
- color: #fff;
- background-color: #7e4fa0;
+ .music-card img {
+ border: none !important;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --music-bg-hover-dark: #525252; /* neutral-600 */
+ --music-bg-dark: rgba(82, 82, 82, 0.5); /* neutral-600/50 */
+ --music-text-secondary: #a3a3a3;
+ }
+ }
+
+ /* THIS SHIT MADE FROM LLM */
+
+ /* --- 主容器 --- */
+ /* flex flex-col gap-2 */
+ .music-card {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ width: 100%;
+ }
+
+ /* --- 信息区域 (左侧) --- */
+ /* flex gap-5 items-center */
+ .music-info {
+ display: flex;
+ align-items: center;
+ gap: 1.25rem;
+ }
+
+ /* 封面图 */
+ /* w-15 h-15 rounded-lg shadow-lg m-0! */
+ .music-cover {
+ width: 3.75rem; /* 15 * 0.25rem */
+ height: 3.75rem;
+ border-radius: 0.5rem;
+ box-shadow: var(--music-shadow);
+ margin: 0 !important;
+ object-fit: cover;
+ }
+
+ /* 文本信息 */
+ .music-title {
+ margin: 0;
+ font-size: 1rem; /* text-size-base */
+ font-weight: bold;
+ line-height: 1.2;
+ }
+
+ .music-artist {
+ margin: 0;
+ font-size: 0.875rem; /* text-size-sm */
+ color: var(--music-text-secondary);
+ }
+
+ /* --- 链接区域 (右侧) --- */
+ /* flex gap-2 items-center */
+ .music-links {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ /* 链接按钮 */
+ /* block rounded-full px-3 transition-colors no-underline! text-size-xs inline-flex gap-1 items-center bg... */
+ .music-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0 0.75rem; /* px-3 */
+ height: 2rem;
+ border-radius: 9999px; /* rounded-full */
+ text-decoration: none !important;
+ font-size: 0.75rem; /* text-size-xs */
+ transition: background-color 0.3s ease;
+
+ /* 浅色模式背景 */
+ background-color: var(--music-bg-light);
+ color: inherit;
+ }
+
+ .music-btn:hover {
+ background-color: var(--music-bg-hover-light);
}
- .site-name {
- transition: 0.3s;
- width: 66px;
- border-radius: 5px;
- text-transform: uppercase;
+ /* 图标 */
+ /* w-3 h-3 m-0! */
+ .music-icon {
+ display: block;
+ width: 0.75rem;
+ height: 0.75rem;
+ margin: 0 !important;
+ }
+
+ /* 按钮文字 */
+ .music-btn-text {
+ display: inline;
+ }
+
+ /* --- 深色模式覆盖 --- */
+ @media (prefers-color-scheme: dark) {
+ .music-btn {
+ background-color: var(--music-bg-dark);
+ }
+ .music-btn:hover {
+ background-color: var(--music-bg-hover-dark);
+ }
+ }
+
+ /* --- 桌面端/大屏适配 (Media Query: md / min-width: 768px) --- */
+ @media (min-width: 768px) {
+ /* 容器变为横向排列,两端对齐 */
+ /* md:flex-row md:justify-between md:items-center */
+ .music-card {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ /* 按钮在大屏下变为方形图标按钮 */
+ /* md:p-4 */
+ .music-btn {
+ padding: 1rem;
+ justify-content: center;
+ width: auto;
+ height: auto;
+ }
+
+ /* 图标变大 */
+ /* md:w-5 md:h-5 */
+ .music-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ }
+
+ /* 文字在大屏下隐藏 */
+ /* md:hidden */
+ .music-btn-text {
+ display: none;
+ }
}
@media (prefers-color-scheme: dark) {
@@ -142,7 +287,8 @@
border: #30363d 1px solid;
}
- .post-title a {
+ .post-title a,
+ .blog-posts a {
color: #fff !important;
}
@@ -166,11 +312,7 @@
}
a {
- font-weight: 700 !important;
- }
-
- a:hover {
- color: #ddd !important;
+ font-weight: 500 !important;
}
}
@@ -216,10 +358,6 @@
font-weight: 600;
}
- a:hover {
- color: #3273dc;
- }
-
.posts-list li {
margin-bottom: 12px;
}
diff --git a/static/js/webmention.js b/static/js/webmention.js
index dc9bc5c..d8cada8 100644
--- a/static/js/webmention.js
+++ b/static/js/webmention.js
@@ -98,124 +98,128 @@ A more detailed example:
// @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);
+ "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,
+ );
}
- let authorPhoto = '';
- if (r.author && r.author.photo) {
- authorPhoto = `
+ /**
+ * 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"
@@ -223,22 +227,22 @@ A more detailed example:
alt="${who}"
>
`;
- } else {
- authorPhoto = `
+ } 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>`;
- }
+ let rsvp = "";
+ if (r.rsvp && rsvpEmoji[r.rsvp]) {
+ rsvp = `<sub>${rsvpEmoji[r.rsvp]}</sub>`;
+ }
- return`
+ return `
<a
class="reaction"
rel="nofollow ugc"
@@ -246,230 +250,229 @@ A more detailed example:
href="${r[mentionSource]}"
>
${authorPhoto}
- ${(reactEmoji[r['wm-property']] || '💥')}
+ ${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(' ');
- }
+ /**
+ * 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("//"));
}
- 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);
+ /**
+ * 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(" ");
+ }
}
- const type = `<span class="${linkclass}">${linktext}</span>`;
+ return text;
+ }
- return `<li>${image} ${link} ${type}</li>`;
- })
- .join('');
- return `
+ /**
+ * 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 `
+ }
+
+ /**
+ * @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 = [];
+ /**
+ * @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;
+ }
- if (commentsAreReactions) {
- comments = collects;
- }
+ const pages = [stripurl(refurl)];
+ if (!!addurls) {
+ addurls.split("|").forEach(function (url) {
+ pages.push(stripurl(url));
+ });
+ }
- /** @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
- };
+ 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);
+ }
- json.children.forEach(function(child) {
- // Map each mention into its respective container
- const store = mapping[child['wm-property']];
- if (store) {
- store.push(child);
- }
- });
+ /** @type {Array<Reaction>} */
+ let comments = [];
+ /** @type {Array<Reaction>} */
+ const collects = [];
- // format the comment-type things
- let formattedComments = '';
- if (comments.length > 0 && comments !== collects) {
- formattedComments = formatComments(dedupe(comments));
- }
+ if (commentsAreReactions) {
+ comments = collects;
+ }
- // format the other reactions
- let reactions = '';
- if (collects.length > 0) {
- reactions = formatReactions(dedupe(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));
+ }
- let finalHtml =`${formattedComments}${reactions}`;
- if(!finalHtml.trim()) {
- finalHtml = '<p class="wm-empty">Nothing.</p>';
- }
+ // format the other reactions
+ let reactions = "";
+ if (collects.length > 0) {
+ reactions = formatReactions(dedupe(collects));
+ }
- // 关键修正:将 finalHtml 变量的内容,赋值给容器的 innerHTML 属性
- container.innerHTML = finalHtml;
+ 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