feat: 论坛 emoji 表情支持
- 新增 dyEmoji 表情库与解析测试、EmojiPicker 选择器与 EmojiImg 组件 - Markdown 编辑器/渲染支持表情图片,新增 public/emoji 资源与生成脚本 - 聊天室与首页适配
This commit is contained in:
100
frontend/lib/dyEmoji.test.ts
Normal file
100
frontend/lib/dyEmoji.test.ts
Normal 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("你好[:猫猫:]!"),
|
||||
`你好!`
|
||||
);
|
||||
});
|
||||
it("合成表情组合名短代码", () => {
|
||||
assert.equal(
|
||||
expandDyEmojiShortcodes("[:001--002--heart:]"),
|
||||
""
|
||||
);
|
||||
});
|
||||
it("多次替换且共享前缀", () => {
|
||||
assert.equal(
|
||||
expandDyEmojiShortcodes("[:猫猫:][:aa:][:猫猫:]"),
|
||||
``
|
||||
);
|
||||
});
|
||||
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第二行"),
|
||||
`第一行\n第二行`
|
||||
);
|
||||
});
|
||||
it("连续使用共享同一正则(lastIndex 复用安全)", () => {
|
||||
assert.equal(expandDyEmojiShortcodes("[:猫猫:]"), ``);
|
||||
assert.equal(expandDyEmojiShortcodes("[:b:]"), "");
|
||||
assert.equal(expandDyEmojiShortcodes("纯文本"), "纯文本");
|
||||
assert.equal(expandDyEmojiShortcodes("[:c:]"), "");
|
||||
});
|
||||
it("正则含 g 标志(供分段渲染使用)", () => {
|
||||
assert.ok(DY_EMOJI_RE.global);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user