feat: 论坛 emoji 表情支持

- 新增 dyEmoji 表情库与解析测试、EmojiPicker 选择器与 EmojiImg 组件
- Markdown 编辑器/渲染支持表情图片,新增 public/emoji 资源与生成脚本
- 聊天室与首页适配
This commit is contained in:
2026-09-28 01:28:41 +08:00
parent 2f96a36275
commit 34aee91a13
3522 changed files with 1560 additions and 251 deletions

View File

@@ -0,0 +1,100 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import {
DY_EMOJI_ALIASES,
DY_EMOJI_GROUPS,
DY_EMOJI_RE,
dyEmojiFileKey,
dyEmojiUrl,
expandDyEmojiShortcodes,
} from "./dyEmoji.ts";
const CAT_HASH = DY_EMOJI_ALIASES["猫猫"];
describe("DY_EMOJI_ALIASES", () => {
it("中文语义名 → hash 文件名,文件名全局唯一", () => {
assert.match(CAT_HASH, /^[A-Za-z0-9_-]+$/);
const files = Object.values(DY_EMOJI_ALIASES);
assert.equal(new Set(files).size, files.length);
});
it("完整覆盖 manifest.tos(集合一致;面板分组顺序独立于 manifest)", () => {
const manifest = JSON.parse(
readFileSync(fileURLToPath(new URL("../public/emoji/dy/manifest.json", import.meta.url)), "utf8")
);
assert.deepEqual(Object.values(DY_EMOJI_ALIASES).sort(), [...manifest.tos].sort());
});
});
describe("DY_EMOJI_GROUPS", () => {
it("分组 keys 无重复", () => {
const all = DY_EMOJI_GROUPS.flatMap((g) => g.keys);
assert.equal(new Set(all).size, all.length);
});
it("分组 keys 恰好覆盖全部中文语义名(不多不少)", () => {
const grouped = DY_EMOJI_GROUPS.flatMap((g) => g.keys).sort();
const allKeys = Object.keys(DY_EMOJI_ALIASES).sort();
assert.deepEqual(grouped, allKeys);
});
});
describe("dyEmojiFileKey / dyEmojiUrl", () => {
it("中文 key 解析为实际文件名,合成表情组合名透传", () => {
assert.equal(dyEmojiFileKey("猫猫"), CAT_HASH);
assert.equal(dyEmojiFileKey("001--002--heart"), "001--002--heart");
});
it("拼接扁平目录 URL", () => {
assert.equal(dyEmojiUrl("猫猫"), `/emoji/dy/${CAT_HASH}.png`);
assert.equal(dyEmojiUrl("001--002--heart"), "/emoji/dy/001--002--heart.png");
});
it("未知 key 原样 URL 编码透传", () => {
assert.equal(dyEmojiUrl("不存在"), `/emoji/dy/${encodeURIComponent("不存在")}.png`);
});
});
describe("expandDyEmojiShortcodes", () => {
it("中文短代码替换为 markdown 图片", () => {
assert.equal(
expandDyEmojiShortcodes("你好[:猫猫:]!"),
`你好![dy:猫猫](/emoji/dy/${CAT_HASH}.png)!`
);
});
it("合成表情组合名短代码", () => {
assert.equal(
expandDyEmojiShortcodes("[:001--002--heart:]"),
"![dy:001--002--heart](/emoji/dy/001--002--heart.png)"
);
});
it("多次替换且共享前缀", () => {
assert.equal(
expandDyEmojiShortcodes("[:猫猫:][:aa:][:猫猫:]"),
`![dy:猫猫](/emoji/dy/${CAT_HASH}.png)![dy:aa](/emoji/dy/aa.png)![dy:猫猫](/emoji/dy/${CAT_HASH}.png)`
);
});
it("无短代码时原样返回", () => {
const s = "普通文本 [a:] :x: [[y]]";
assert.equal(expandDyEmojiShortcodes(s), s);
assert.equal(expandDyEmojiShortcodes(""), "");
});
it("非法 key 不匹配(含 . / 空 / @ / 空白)", () => {
assert.equal(expandDyEmojiShortcodes("[:a.b:][:]"), "[:a.b:][:]");
assert.equal(expandDyEmojiShortcodes("[:空 格:]"), "[:空 格:]");
assert.equal(expandDyEmojiShortcodes("[:@x:]"), "[:@x:]");
});
it("跨行文本", () => {
assert.equal(
expandDyEmojiShortcodes("第一行[:猫猫:]\n第二行"),
`第一行![dy:猫猫](/emoji/dy/${CAT_HASH}.png)\n第二行`
);
});
it("连续使用共享同一正则(lastIndex 复用安全)", () => {
assert.equal(expandDyEmojiShortcodes("[:猫猫:]"), `![dy:猫猫](/emoji/dy/${CAT_HASH}.png)`);
assert.equal(expandDyEmojiShortcodes("[:b:]"), "![dy:b](/emoji/dy/b.png)");
assert.equal(expandDyEmojiShortcodes("纯文本"), "纯文本");
assert.equal(expandDyEmojiShortcodes("[:c:]"), "![dy:c](/emoji/dy/c.png)");
});
it("正则含 g 标志(供分段渲染使用)", () => {
assert.ok(DY_EMOJI_RE.global);
});
});

341
frontend/lib/dyEmoji.ts Normal file
View File

@@ -0,0 +1,341 @@
/**
* 抖音表情共享逻辑:短代码正则 / URL / 展开为 markdown 图片 / span 收集。
* 纯逻辑模块(无 React 依赖),服务端与客户端组件均可安全引入。
*/
/**
* 抖音表情短代码:[:key:]。key 有两种形态:
* - 默认表情:中文语义名(如 猫猫),经 DY_EMOJI_ALIASES 映射到实际文件
* - 合成表情:组合文件名(fus),原样透传
* 字符集排除 [ ] : 空白 . @(. 防止误吞文件扩展名写法;@ 保证与 @提及 span 不重叠)
*/
export const DY_EMOJI_RE = /\[:([^\[\]:\n\s.@]+):\]/g;
export const DY_EMOJI_BASE = "/emoji/dy";
/**
* 默认表情语义别名(人工看图命名),顺序与 manifest.json 的 tos 一致。
* 面板「默认」tab 与消息内容均以中文短代码为准;更新表情资产时需同步维护本表。
* 用有序数组做单一数据源:对象键会把整数型 key(如 "666")重排到最前,不能依赖键顺序。
*/
const DY_EMOJI_ALIAS_PAIRS: readonly (readonly [string, string])[] = [
["猫猫", "003d8daaa58e4a2fb324bcd3baf4c785"],
["大哭", "0043de2fff65408ab7522538f7b3292a"],
["晕", "01ccc692f9094a0f9839706665c7e8bb"],
["炸弹", "02a91594d67e462faf7c77e827a16eec"],
["禁止", "02fcc587c8b4499080e0c03994967511"],
["5V5", "032007fd6c254a72a162d8c5bf78d512"],
["坏笑", "0431a35382c641679fe74f690241b533"],
["纸飞机", "0636e634f58f4410a8b22675d36f52a9"],
["玫瑰", "06abb0913446422cbac6ab18ea8fa44a"],
["翻白眼", "06df57cc81954ef8bf65dad06a01a69a"],
["无语", "072caed2f2bd4ed5ba35142965845d85"],
["摸头", "0795718a5fe247b19ff80b064d02c66d"],
["礼物", "08d888562e1f4ea086fc4f3eff2ef517"],
["火苗", "094c1f852c2343658b2677023755249b"],
["嘟嘴", "0e13c8681abe4239b3d468635b723d40"],
["心碎", "0eb6e5cde36346289150a83a118bfa62"],
["精选", "1482ac09926b45a1bab8f1dc98a68d22"],
["笑哭", "167a2c32c7874da889fc902f8cf4500a"],
["不屑", "1691cd385ecd4fb6b888d000900d74bc"],
["鞭炮", "16c850aade5b475fa83b67e0266f0222"],
["庆祝", "18d02bf442484371bb918e1f07c43a05"],
["粽子", "1e8aef1795db468682f8b63521834f39"],
["酷哭", "1eabd1db85dd4bddbbd842abe9cdce49"],
["大喊", "1ec163608a2a49ffbbc766e505885bb1"],
["怒", "1edc3de7713647c2b269f7f48b494b2d"],
["发奋", "1f71ffdc4df041ac836cdc81940f68ff"],
["OK", "21b1e1cdd15742e9aacc6cfd764260cc"],
["气球", "24a048472a4c429c8d01d0401e846285"],
["抱抱", "24b237f43dee47c5a068bf4457b97145"],
["生气", "2609e5a35a3f436084bd618ef819227f"],
["卖萌", "2618a9c1f9424c78aaf7e6174f0242b4"],
["尴尬", "28d74529207c43178cc72cba847daf12"],
["得意", "29288834d417481f97eb63fee5c30b53"],
["凋谢", "2b7cfe4c3c524b709bd3f15537c06911"],
["大笑", "2d1fca45aceb46b8b3a8d55084977851"],
["太阳", "2fb7d94d6e7642349793def00e5b3e0c"],
["彩带", "30d222de7c4a42cdb6d7a9ea7bea162b"],
["睡觉", "30d9ccb7e4ae4a43997e76fbabd6b5cd"],
["奸笑", "3139a081129f4832a70da8d7808114fa"],
["OK手", "317a9b7fe5f94023a8b2987173708362"],
["露齿笑", "33745171e4a544ad913b52ebb6b46b60"],
["捂脸笑", "3545637c30284f6bb732a444f91196ab"],
["吐舌", "365405db173b45978ea6a45bd47e9d87"],
["手枪", "3b366738018343ae9831a79fa17b3dae"],
["惊恐", "3c8520db7cbd418f95aea788ee6a7cf0"],
["舔屏", "3d13332de5da45498cd1ef193cc4afff"],
["捂脸", "3df5f3ca07e546b697b301ab9e2e3c23"],
["难受", "3e8a89ca5dc74cb88b4000551a1312a7"],
["眯眼", "4201288b0e1b47b59f9f031a53c3d687"],
["嘿嘿", "42fd189ab7ad4f478f220d34046916fb"],
["亲亲", "434e5830fe874fdca427b0d8b23a751d"],
["花痴", "4364693f63a34e2999c54cbbc75df4e9"],
["眨眼", "44ddc9a6bcd9471a858a33c382f6fafa"],
["鸡腿", "45a1632fd059454b91c71d8d54e46a1b"],
["小蓝", "45f8ce9a22b0466fbcd1bb2a841d2225"],
["愤怒", "476e92daceb14bee99e36b8f50a40e81"],
["祈祷", "478afb0911604f5289d9cbfd4183b404"],
["贱笑", "47eb8964fef7479c80a1088f405beb43"],
["汗", "47fe86652a1b4cf288de5f929cba5d80"],
["擦汗", "49b5a54103ae43b0b02a564a3feec5a8"],
["思考", "49bb5e7832d644dd99f50d3b6733a4c6"],
["咖啡", "4b5f538005d148248338d97c47e20f9e"],
["晕圈", "4c0f3ba463734e50981103650f440be0"],
["点赞", "4c4a175aaf2b4e0c98b8d46684d64564"],
["可怜", "4cd68b81179d421ebaf2c4e36b3630c6"],
["开心", "4ec5e9c2001249b79bf4ff0c6a431cae"],
["圣诞帽", "4f1aeeedf61a4051aaf0bc96a6ead49f"],
["爱心眼", "4f6a86e4bfd14f8c8434f6d643782a41"],
["碰拳", "4f815c8c1795412cbb67233685d785a1"],
["冒烟", "517f7385630e4641af73c3ea93e44c7d"],
["委屈", "52aeca47432745729c08609d46211ed1"],
["指左", "54994d1e88d94722a71960508a8e9ce6"],
["二哈", "54afb78ebf6a45a09c6dc7d77fc3d3fb"],
["加油", "54e9a10981a242dba426a4f63d4103ab"],
["疑问", "55656c6385754a35acce9f807c4afa32"],
["惊呆", "55c305ac8ebe4f2a8e5fed639f188614"],
["笑泪", "590d12b8677d4738ad9e989ea0280f81"],
["绝", "5aaaff01fd1f493a82821b1a3943bf68"],
["香蕉", "5b0b93a490394665bed2a52ae67a2975"],
["福", "5cdc7b1819c3484db61fa60eeb989e2e"],
["捧腹", "5d7d5f3c3f9548679fb73090a69f538a"],
["红包", "5defde4959044aaeae628e060e82043f"],
["猫震惊", "5faa8e1516204480b74b6f6a3122d578"],
["呆", "600e9cfaf3a14174b451e2206baf1e0f"],
["柴犬", "603e30989b05474788e3709d7eebd4ef"],
["666", "61267a18cbf847b3af73190bc52ae36a"],
["握手", "613459694fa24ae689aa306fd07944f8"],
["吐", "616147770ba044a3abed4224c46fe605"],
["惊讶", "622ddf449a5d4ac6a0e15445356cfb94"],
["套头", "626ec790acdf4c10b5309a99ae75ecb1"],
["嫌弃", "62d34256ab7845709616c5a7bf42aee1"],
["给力", "63b22c1e04f34e8a8816ba2b8ac46ae3"],
["斜眼", "6572851608ba4ab3acc0f0c879594d1b"],
["星星脸", "66fb1287c3f04b4bbb427a1368d12a22"],
["蓝怪", "6b280673ae0740e78d82178ee018a811"],
["猪猪", "6c3589f8daec4143bd9b9d9b41e802ec"],
["点踩", "6c80753d816442718d2f5ca44e1eb13a"],
["关注我", "6ed70f4a306a48bcbc12a138165fb09d"],
["巧克力", "71e20043b5f34b03b7dd85afb69f0503"],
["调皮", "72045b3ac8654458a94767f463804c82"],
["红心", "740e29f4ca6547ccb4f7cf3fe0463629"],
["泪喷", "770948b40fe14c51a73e18dff5a8d29c"],
["红唇", "77a76f873a1f4e6ba5eeff343391b213"],
["怪笑", "77e480a1a9234d94bfcab3429f5f96bb"],
["钻戒", "7d2da9bea4f0477aa530340355fda832"],
["崩溃", "7e606e0d73d843a2bc1c15219a1318cd"],
["绿帽", "7e79587db89a4404a0ebcf4aabba3d2d"],
["圣诞树", "8260b9fef99e430aa08ff9259355e907"],
["敲打", "831ef27451104b0e9eb89660c3d7b716"],
["龇牙", "86d772fc25a0404090613bad4a123947"],
["扎心", "8a054c74604844498fa9c4ae809f1c79"],
["爱心枪", "8a7a9fb41d444aa68dd5568ba72040aa"],
["心动", "8bb95fd220bf4efeb44c9b3e6c63afdf"],
["墨镜笑", "8cd39208016c428ca80d3468e24edb0a"],
["贴贴", "8dcb4606e9f14613a3bbf98a1646e56f"],
["微笑", "907e61eb04e54074a101b43035d50145"],
["挨揍", "91656d99d6d645a5995187454e3293f4"],
["耳机", "91aa9af22f664ba08ca3c66184f2a838"],
["打响指", "94266b5d083d45afb4309ed6ed905dd6"],
["转眼", "94c011b83a3e40cbbcfeb09c6adb2b43"],
["狂笑", "9501220f825547d699c8409184846166"],
["麻将", "956e469de272416ebeb0aafd780bd78e"],
["雪花", "98c4f361b8cc49e2aeff0b546664af42"],
["飞吻", "9a0887a55ff94cbbbadbbbdb14c7396a"],
["指右", "9a35f66a62804e90b31455ab6f0d99ee"],
["沉思", "9e2b0289cd994a91a77676265d5ab7a7"],
["裂开", "9eb467c1c4d849f39b45ad4e7beff0ce"],
["烟花", "9ebf943dbb974a5e8dd13e5a339cff18"],
["仓鼠", "9ff343dc38e54a9f98e7fa896dd95715"],
["揉脸", "a3d1ce9301c5410d88cc2470a3e11c30"],
["鼓掌", "a4d28f23c10e4efaa979b4d119b7ade4"],
["黑脸", "a4d9f336b64f4554a0790eed0f0e1a71"],
["数钱", "a7a5a4db025642a392ee10d5c74e65c9"],
["哇", "a90f7139a14140dbba575425193ca6c5"],
["尬笑", "a993e64d05cd428e876600a75129332f"],
["舒心", "ab54a06f377346f88f094fe6281103ff"],
["作揖", "abf7cc107d4740e4bc1e238b770ac889"],
["窃喜", "aca47d7bc40a442b933812e3f482549d"],
["跌倒", "aceb3d3827574b68a40f7c7ebf1e3049"],
["楚楚", "ad1d6ca9bc694b78b8b3ad816aaccf0d"],
["拍脸", "ada62e0815f141cdbdc5a76e6781786d"],
["击掌", "ae9cd379b8c448d5a3c3aa107c3272d8"],
["香吻", "aec920e3a21444c79a79defd76a45a75"],
["抓狂", "afa666d51d474fc6b2f0a10aca57c372"],
["摇扇", "b1c3d09acfe84b9280a28c4190fa0056"],
["doge", "b217d170f0c349fc8ea86615738a5c81"],
["咋舌", "b21a518ebd8b48caab9c814e40b63115"],
["咧嘴", "b341f7cdcaea452a83a78eb81b5a30b4"],
["掩面", "b379ea709e484581830373adf1bce572"],
["狗拳", "b485611a693d4eadadf29f5f055511f6"],
["糖葫芦", "b67a198b447a45d0a36041196e26e447"],
["黄瓜", "ba6c382121e94f52bb44f41b9156f447"],
["脸红", "bbcb745bbb1c463089a0d40177623c9e"],
["叫好", "bd1068daba564862be71631451611ba7"],
["白菜狗", "bd5541fc02514e7e8aefde9b0aabc652"],
["捂嘴", "bf6b3de54ab34732af82809635c704f8"],
["互粉", "bfa5963893bd41519ff31c7415f6956f"],
["蛋糕", "c08640142c304d76959ee198cc8a2efd"],
["撇嘴", "c10ec8abb6da4adbb3388d435a980568"],
["求机位", "c5c18a6bfb3d43f9a96f833f9a758d71"],
["心心眼", "c6605942715f4717805101c311b41f24"],
["蓝毛", "c6646c7cd6fc4f8d89f332c2d46e4597"],
["元宝", "c67bf9ed7bb34e00af6ef863d6ecfdad"],
["吐血", "c98569a7f04943d7a8de0c15cef36151"],
["便便", "cabe6bcd9889449daa87c2df765f379e"],
["鞠躬", "cb9f16ad45d84f7ca4256f20048e422a"],
["苹果", "cc5884dd4b814d81a9982bbcee136e45"],
["痴笑", "cca02095fe5f448ea2289ece0e2ac49f"],
["哈士奇", "cce25443e61846699d1f08782fb365bb"],
["猜拳", "cd619c9151a14094ab32f44f7487b980"],
["棒棒糖", "cd8647d2106b48fd9142c9c41562c8f7"],
["掩嘴", "cf1a3c5b317f4c0399ed39888d73abe7"],
["彩虹", "cfeddc926d3846eabda23e750364e82d"],
["酷脸", "d15c89c9f3364485b514d51b43b16771"],
["吃橘", "d2a361001193458ab3250ab557c7ec4b"],
["西瓜", "d2c0b2c0484b4bd59d6b266c5a80c199"],
["躺平", "d3084c0ac6e74eae84dd39cbc26d990d"],
["掩脸", "d34c2cb72f9f45a1bd21669f832b9a94"],
["鬼脸", "d3cbf267d0d8426e90edfb9c75059314"],
["加油牌", "d525a68955854abda1c8bdf7f62ebadb"],
["出拳", "d8a316cb0427498b87e396350b83b504"],
["加一", "d9840b29c0d84082b758580ff6da0281"],
["吃瓜", "db6bb519e6bc4b9b819e04b05e194586"],
["瞪眼", "dcaa1bb24fed447b9ef155462db2a890"],
["黑球", "ddd30bbe0a194430af6bcd6fef98570f"],
["狼狗", "de102f5602824afb88009091971a862d"],
["猫哭", "df7b9c648eef4720962f604f1c81a77c"],
["捂眼", "dfaa3f289fd54142ad6c8088ee516852"],
["蹲下", "e44b432ef18e4d84844e9eb1e7d135ac"],
["干笑", "e4b71ab984154f0ab1709bd88e9defda"],
["不悦", "e5275fd92a234e34984044e3b2213ebc"],
["捧脸", "e58ab050664b44eaa7a7c8b8d6fe9cf7"],
["羞涩", "e7f9e93ae5da435884e17c57442729f1"],
["盯", "e80c05fa6c4943759dba8486512901e0"],
["开怀", "e9078540aac945b4992ae19dbf4d0783"],
["红酒", "e959a423dd0a43fd8fcd5dd0499928cf"],
["灯笼", "ea0d63f135594b82ac3e386cf684046a"],
["干杯", "ea97458fd890435c9fa6df3520f66b1f"],
["军帽", "eb8d8e8a04e34db082437c083539dd2f"],
["禁烟花", "ebff2644a4ab4149a41f1bc7282879b1"],
["减一", "efa80a6e10f04dcc8de89092744b2bf9"],
["叼玫瑰", "f1d000c0d62a4d77b14641076a05f5b9"],
["嚣张", "f266b481b9e14e588228016138913c75"],
["拍我", "f2efb004f2f842b1bc20284bd2e010e3"],
["干饭", "f68c5e1160c449508d55cbe2acc0bd47"],
["德牧", "f730fe9f0bd44b59b8f0ed5069a747dc"],
["糖果", "f75de5be772c458e9a5469b32c75413e"],
["甜点", "f840c0bdd606425a8dd1190b80126674"],
["比心", "f87a6fb3ac774f2e8f9f1760e3d2de45"],
["迷恋", "fba0f99d4b8049cd905bfd076d4efbf2"],
["惊呼", "fba1c2ac23394a31bc1c3405148bd363"],
["郁闷", "fbbbf98a169642d386020f79d06af11c"],
["含泪", "fbd4350861fe49cd991e76e82e382715"],
["三文鱼", "fd24c9f2258f4640af610472b442f9ec"],
];
/**
* 默认表情分组:同类型相邻排列(参考抖音/飞书面板)。
* keys 必须恰好覆盖 DY_EMOJI_ALIAS_PAIRS 的全部中文 key 且不重复(有单测锁定)。
*/
export const DY_EMOJI_GROUPS: readonly { label: string; keys: string[] }[] = [
{
label: "笑脸",
keys: [
"微笑", "露齿笑", "笑哭", "笑泪", "捂脸笑", "嘿嘿", "贱笑", "奸笑", "怪笑", "痴笑",
"狂笑", "大笑", "捧腹", "开怀", "窃喜", "尬笑", "干笑", "咧嘴", "龇牙", "吐舌",
"舔屏", "星星脸", "花痴", "得意", "坏笑", "墨镜笑", "嚣张", "酷哭", "酷脸", "调皮",
"卖萌", "呆", "鬼脸", "脸红", "羞涩", "楚楚", "捧脸", "舒心", "开心", "精选",
],
},
{
label: "情绪",
keys: [
"大哭", "含泪", "泪喷", "委屈", "难受", "郁闷", "不悦", "无语", "汗", "擦汗",
"晕", "晕圈", "崩溃", "抓狂", "怒", "生气", "愤怒", "黑脸", "尴尬", "可怜",
"裂开", "吐", "吐血", "嘟嘴", "撇嘴", "嫌弃", "不屑", "翻白眼", "斜眼", "哇",
"大喊", "惊呆", "惊讶", "惊呼", "惊恐", "瞪眼", "盯", "眨眼", "眯眼", "转眼",
"疑问", "思考", "沉思", "咋舌", "绝", "睡觉", "躺平",
],
},
{
label: "手势",
keys: [
"OK", "OK手", "点赞", "点踩", "碰拳", "击掌", "出拳", "猜拳", "狗拳", "握手",
"作揖", "鞠躬", "祈祷", "打响指", "鼓掌", "加油", "加油牌", "发奋", "指左", "指右",
"摸头", "揉脸", "捂脸", "掩面", "掩脸", "掩嘴", "捂嘴", "捂眼", "拍脸", "拍我",
"套头", "挨揍", "跌倒", "蹲下", "抱抱",
],
},
{
label: "爱心",
keys: [
"红心", "心碎", "爱心眼", "心心眼", "爱心枪", "心动", "比心", "迷恋", "贴贴", "亲亲",
"香吻", "飞吻", "红唇", "绿帽",
],
},
{
label: "动物",
keys: [
"猫猫", "猫震惊", "猫哭", "二哈", "哈士奇", "柴犬", "白菜狗", "德牧", "狼狗", "猪猪",
"仓鼠", "小蓝", "蓝怪", "蓝毛", "三文鱼", "doge",
],
},
{
label: "食物",
keys: [
"鸡腿", "咖啡", "香蕉", "苹果", "西瓜", "黄瓜", "吃瓜", "吃橘", "糖葫芦", "棒棒糖",
"巧克力", "蛋糕", "糖果", "甜点", "粽子", "红酒", "干饭", "干杯",
],
},
{
label: "物品",
keys: [
"炸弹", "禁止", "手枪", "火苗", "冒烟", "太阳", "雪花", "彩虹", "气球", "纸飞机",
"玫瑰", "凋谢", "叼玫瑰", "礼物", "钻戒", "元宝", "红包", "数钱", "便便", "黑球",
"耳机", "摇扇", "麻将", "军帽", "圣诞帽", "圣诞树", "灯笼", "福", "鞭炮", "烟花",
"禁烟花", "彩带", "庆祝", "叫好", "求机位", "关注我", "互粉", "5V5", "666", "加一",
"减一", "给力", "敲打", "扎心",
],
},
];
/** 中文语义名 → 实际文件名查找表 */
export const DY_EMOJI_ALIASES: Record<string, string> =
Object.fromEntries(DY_EMOJI_ALIAS_PAIRS);
/** key → 实际文件名:中文语义名查别名表,合成表情组合名原样透传 */
export function dyEmojiFileKey(key: string): string {
return DY_EMOJI_ALIASES[key] ?? key;
}
export function dyEmojiUrl(key: string): string {
return `${DY_EMOJI_BASE}/${encodeURIComponent(dyEmojiFileKey(key))}.png`;
}
/**
* 把 [:key:] 短代码展开为 markdown 行内图片(供 Markdown 渲染管线)。
* 注意:不感知代码围栏,围栏内的短代码也会被展开(v1 已知限制)。
*/
export function expandDyEmojiShortcodes(text: string): string {
if (!text || !text.includes("[:")) return text;
return text.replace(DY_EMOJI_RE, (_m, key: string) => {
return `![dy:${key}](${dyEmojiUrl(key)})`;
});
}
export type DyEmojiManifest = { tos: string[]; fus: string[] };
/** 收集文本中的表情短代码位置(聊天等纯文本渲染用;每次用独立正则避免 g 的 lastIndex 状态) */
export function collectDyEmojiSpans(
text: string
): { start: number; end: number; key: string }[] {
if (!text || !text.includes("[:")) return [];
const spans: { start: number; end: number; key: string }[] = [];
const re = new RegExp(DY_EMOJI_RE.source, "g");
for (let m = re.exec(text); m; m = re.exec(text)) {
spans.push({ start: m.index, end: m.index + m[0].length, key: m[1] });
}
return spans;
}