@xihe-lab/tapd-core 2.0.0-rc.3 → 2.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/richtext/index.d.ts +1 -14
- package/dist/richtext/index.d.ts.map +1 -1
- package/dist/richtext/index.js +3 -34
- package/dist/richtext/index.js.map +1 -1
- package/dist/richtext/pipeline.d.ts +1 -4
- package/dist/richtext/pipeline.d.ts.map +1 -1
- package/dist/richtext/pipeline.js +2 -198
- package/dist/richtext/pipeline.js.map +1 -1
- package/package.json +3 -8
- package/dist/richtext/attachment-anchor.d.ts +0 -58
- package/dist/richtext/attachment-anchor.d.ts.map +0 -1
- package/dist/richtext/attachment-anchor.js +0 -221
- package/dist/richtext/attachment-anchor.js.map +0 -1
- package/dist/richtext/attachment-anchor.test.d.ts +0 -2
- package/dist/richtext/attachment-anchor.test.d.ts.map +0 -1
- package/dist/richtext/attachment-anchor.test.js +0 -149
- package/dist/richtext/attachment-anchor.test.js.map +0 -1
- package/dist/richtext/escape.d.ts +0 -7
- package/dist/richtext/escape.d.ts.map +0 -1
- package/dist/richtext/escape.js +0 -60
- package/dist/richtext/escape.js.map +0 -1
- package/dist/richtext/mention.d.ts +0 -49
- package/dist/richtext/mention.d.ts.map +0 -1
- package/dist/richtext/mention.js +0 -179
- package/dist/richtext/mention.js.map +0 -1
- package/dist/richtext/mention.test.d.ts +0 -2
- package/dist/richtext/mention.test.d.ts.map +0 -1
- package/dist/richtext/mention.test.js +0 -123
- package/dist/richtext/mention.test.js.map +0 -1
- package/dist/richtext/round-trip.test.d.ts +0 -2
- package/dist/richtext/round-trip.test.d.ts.map +0 -1
- package/dist/richtext/round-trip.test.js +0 -105
- package/dist/richtext/round-trip.test.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-anchor.test.d.ts","sourceRoot":"","sources":["../../src/richtext/attachment-anchor.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// attachment-anchor 语义单测:fileTypeFor 映射 / md 扩展语法 / R6 五件套模板 / 解析容错 / 不认识锚点原样保留。
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { ATTACH_SCHEME, DEFAULT_ATTACHMENT_SLUG, FILE_TYPE_BY_EXTENSION, attachmentAnchorHtml, attachmentAnchorMd, attachmentHtmlToMd, attachmentMdToHtml, fileTypeFor, findAttachmentAnchors, isAttachmentAnchorNode, attachmentAnchorNodeToMd, parseAttachmentAnchorHtml, parseAttachmentAnchorMd, } from './attachment-anchor.js';
|
|
4
|
-
const WS = '39814312';
|
|
5
|
-
const ID = '1139814312001000001';
|
|
6
|
-
// 完整编辑器模板(含 inner spans / type / class,e2e 金样 poc-rc2-file-embed.mjs 同构)
|
|
7
|
-
const EDITOR_STYLE_ANCHOR = `<a data-is-tapd-attachment="true" contenteditable="false" target="_blank" rel="noopener" ` +
|
|
8
|
-
`data-can-preview="true" data-file-type="text" type="text" data-name="comment-embed.txt" data-size="54" ` +
|
|
9
|
-
`href="/${WS}/attachments/preview_attachments/1139814312001000004/story_description_attachment" ` +
|
|
10
|
-
`class="tapd-editor__attachment-item"><span class="tapd-editor__attachment-item__left">` +
|
|
11
|
-
`<span class="tapd-editor__attachment-item__text-wrapper">` +
|
|
12
|
-
`<span class="tapd-editor__attachment-item__title">comment-embed.txt</span>` +
|
|
13
|
-
`<span class="tapd-editor__attachment-item__size">0.00MB</span></span></span></a>`;
|
|
14
|
-
const checks = [
|
|
15
|
-
['fileTypeFor: txt→text、png/jpg/jpeg→image、pdf→pdf、其他→other(大小写不敏感)', () => {
|
|
16
|
-
assert.equal(fileTypeFor('说明.txt'), 'text');
|
|
17
|
-
assert.equal(fileTypeFor('pic.PNG'), 'image');
|
|
18
|
-
assert.equal(fileTypeFor('a.jpg'), 'image');
|
|
19
|
-
assert.equal(fileTypeFor('a.jpeg'), 'image');
|
|
20
|
-
assert.equal(fileTypeFor('doc.pdf'), 'pdf');
|
|
21
|
-
assert.equal(fileTypeFor('app.log'), 'other');
|
|
22
|
-
assert.equal(fileTypeFor('noext'), 'other');
|
|
23
|
-
assert.equal(fileTypeFor('报表@v2.pdf'), 'pdf');
|
|
24
|
-
}],
|
|
25
|
-
['FILE_TYPE_BY_EXTENSION: 映射表导出供 Wave 2 复用', () => {
|
|
26
|
-
assert.equal(FILE_TYPE_BY_EXTENSION.txt, 'text');
|
|
27
|
-
assert.equal(FILE_TYPE_BY_EXTENSION.png, 'image');
|
|
28
|
-
assert.equal(FILE_TYPE_BY_EXTENSION.pdf, 'pdf');
|
|
29
|
-
assert.equal(ATTACH_SCHEME, 'attach:');
|
|
30
|
-
assert.equal(DEFAULT_ATTACHMENT_SLUG, 'story_description_attachment');
|
|
31
|
-
}],
|
|
32
|
-
['attachmentAnchorMd: md 扩展语法生成', () => {
|
|
33
|
-
assert.equal(attachmentAnchorMd({ workspaceId: WS, id: ID, name: '需求说明.txt' }), `[📎 需求说明.txt](attach:${WS}/${ID})`);
|
|
34
|
-
}],
|
|
35
|
-
['attachmentAnchorMd: 文件名中 []\\ 转义可逆;非法 ws/id 抛错', () => {
|
|
36
|
-
const md = attachmentAnchorMd({ workspaceId: WS, id: ID, name: 'a]b[ c\\d.txt' });
|
|
37
|
-
assert.equal(md, `[📎 a\\]b\\[ c\\\\d.txt](attach:${WS}/${ID})`);
|
|
38
|
-
const parsed = parseAttachmentAnchorMd(md);
|
|
39
|
-
assert.equal(parsed?.name, 'a]b[ c\\d.txt');
|
|
40
|
-
assert.throws(() => attachmentAnchorMd({ workspaceId: 'ws x', id: ID, name: 'f' }), TypeError);
|
|
41
|
-
assert.throws(() => attachmentAnchorMd({ workspaceId: WS, id: '', name: 'f' }), TypeError);
|
|
42
|
-
}],
|
|
43
|
-
['parseAttachmentAnchorMd: 非 attach: 链接与普通链接 → null', () => {
|
|
44
|
-
assert.equal(parseAttachmentAnchorMd('[标题](https://www.tapd.cn/x)'), null);
|
|
45
|
-
assert.equal(parseAttachmentAnchorMd('[📎 x](https://a/b)'), null);
|
|
46
|
-
assert.equal(parseAttachmentAnchorMd('plain text'), null);
|
|
47
|
-
}],
|
|
48
|
-
['parseAttachmentAnchorMd: 完整解析 ws/id/name', () => {
|
|
49
|
-
const ref = parseAttachmentAnchorMd(`前缀 [📎 需求说明.txt](attach:${WS}/${ID}) 后缀`);
|
|
50
|
-
assert.deepEqual(ref, { workspaceId: WS, id: ID, name: '需求说明.txt' });
|
|
51
|
-
}],
|
|
52
|
-
['attachmentAnchorHtml: R6 定稿最小模板逐字段(含 size)', () => {
|
|
53
|
-
assert.equal(attachmentAnchorHtml({ workspaceId: WS, id: ID, name: '接口设计.pdf', size: 2048 }), `<a data-is-tapd-attachment="true" data-can-preview="true" data-file-type="pdf" ` +
|
|
54
|
-
`data-name="接口设计.pdf" data-size="2048" target="_blank" rel="noopener" ` +
|
|
55
|
-
`href="/${WS}/attachments/preview_attachments/${ID}/story_description_attachment">接口设计.pdf</a>`);
|
|
56
|
-
}],
|
|
57
|
-
['attachmentAnchorHtml: size 省略不输出 data-size;fileType 按扩展名默认映射;slug 可覆盖', () => {
|
|
58
|
-
const html = attachmentAnchorHtml({ workspaceId: WS, id: ID, name: 'app.log' });
|
|
59
|
-
assert.ok(html.includes('data-file-type="other"'));
|
|
60
|
-
assert.ok(!html.includes('data-size'));
|
|
61
|
-
assert.ok(html.includes(`href="/${WS}/attachments/preview_attachments/${ID}/story_description_attachment"`));
|
|
62
|
-
const custom = attachmentAnchorHtml({ workspaceId: WS, id: ID, name: 'a.txt', fileType: 'text' }, { slug: 'bug_description_attachment' });
|
|
63
|
-
assert.ok(custom.includes('/bug_description_attachment">'));
|
|
64
|
-
assert.ok(custom.endsWith('>a.txt</a>'));
|
|
65
|
-
}],
|
|
66
|
-
['attachmentAnchorHtml: 文件名 HTML 特殊字符转义', () => {
|
|
67
|
-
const html = attachmentAnchorHtml({ workspaceId: WS, id: ID, name: 'a"b&c.txt' });
|
|
68
|
-
assert.ok(html.includes('data-name="a"b&c.txt"'));
|
|
69
|
-
assert.ok(html.endsWith('>a"b&c.txt</a>') === false);
|
|
70
|
-
assert.ok(html.endsWith('>a"b&c.txt</a>'));
|
|
71
|
-
}],
|
|
72
|
-
['parseAttachmentAnchorHtml: 生成结果可逆解析', () => {
|
|
73
|
-
const html = attachmentAnchorHtml({ workspaceId: WS, id: ID, name: '接口设计.pdf', size: 2048 });
|
|
74
|
-
assert.deepEqual(parseAttachmentAnchorHtml(html), {
|
|
75
|
-
workspaceId: WS, id: ID, name: '接口设计.pdf', size: 2048, fileType: 'pdf',
|
|
76
|
-
});
|
|
77
|
-
}],
|
|
78
|
-
['parseAttachmentAnchorHtml: 完整编辑器模板(inner spans/type/class)可识别', () => {
|
|
79
|
-
assert.deepEqual(parseAttachmentAnchorHtml(EDITOR_STYLE_ANCHOR), {
|
|
80
|
-
workspaceId: WS, id: '1139814312001000004', name: 'comment-embed.txt', size: 54, fileType: 'text',
|
|
81
|
-
});
|
|
82
|
-
assert.deepEqual(findAttachmentAnchors(`<p>前</p>${EDITOR_STYLE_ANCHOR}<p>后</p>`).length, 1);
|
|
83
|
-
}],
|
|
84
|
-
['parseAttachmentAnchorHtml: 不认识的锚点 → null(裸 <a>/缺 data-name/缺 flag)', () => {
|
|
85
|
-
assert.equal(parseAttachmentAnchorHtml(`<a href="/${WS}/attachments/preview_attachments/${ID}/story_description_attachment">x.txt</a>`), null);
|
|
86
|
-
assert.equal(parseAttachmentAnchorHtml(`<a data-is-tapd-attachment="true" href="/${WS}/attachments/preview_attachments/${ID}/story_description_attachment">残缺</a>`), null);
|
|
87
|
-
assert.equal(parseAttachmentAnchorHtml(`<a data-can-preview="true" data-name="x.txt" href="/${WS}/attachments/preview_attachments/${ID}/story_description_attachment">无flag</a>`), null);
|
|
88
|
-
assert.equal(parseAttachmentAnchorHtml(`<a data-is-tapd-attachment="true" data-name="x.txt" href="https://elsewhere/x">外部链接</a>`), null);
|
|
89
|
-
}],
|
|
90
|
-
['attachmentHtmlToMd: 锚点 → md 扩展语法;不认识锚点原样保留', () => {
|
|
91
|
-
const md = attachmentHtmlToMd(`<p>材料</p>${EDITOR_STYLE_ANCHOR}`);
|
|
92
|
-
assert.equal(md, `<p>材料</p>[📎 comment-embed.txt](attach:${WS}/1139814312001000004)`);
|
|
93
|
-
const bare = `<a href="/${WS}/attachments/preview_attachments/${ID}/story_description_attachment">残缺</a>`;
|
|
94
|
-
assert.equal(attachmentHtmlToMd(`<p>x</p>${bare}`), `<p>x</p>${bare}`);
|
|
95
|
-
const plain = '<p>普通 <a href="https://e.com">链接</a></p>';
|
|
96
|
-
assert.equal(attachmentHtmlToMd(plain), plain);
|
|
97
|
-
}],
|
|
98
|
-
['attachmentMdToHtml: md → 锚点;非 attach 链接透传', () => {
|
|
99
|
-
const html = attachmentMdToHtml(`见 [📎 需求说明.txt](attach:${WS}/${ID})`);
|
|
100
|
-
assert.ok(html.startsWith('见 <a data-is-tapd-attachment="true"'));
|
|
101
|
-
assert.ok(html.includes('data-name="需求说明.txt"'));
|
|
102
|
-
assert.ok(html.endsWith(`>需求说明.txt</a>`));
|
|
103
|
-
const link = '参考 [标题](https://e.com/a)';
|
|
104
|
-
assert.equal(attachmentMdToHtml(link), link);
|
|
105
|
-
}],
|
|
106
|
-
['attachmentMdToHtml: 多锚点与转义名可逆', () => {
|
|
107
|
-
const md = `[📎 a.txt](attach:${WS}/1139814312001000002) 中间 [📎 b.pdf](attach:${WS}/${ID})`;
|
|
108
|
-
const html = attachmentMdToHtml(md);
|
|
109
|
-
assert.equal(findAttachmentAnchors(html).length, 2);
|
|
110
|
-
assert.equal(attachmentHtmlToMd(html), md);
|
|
111
|
-
}],
|
|
112
|
-
['节点辅助: isAttachmentAnchorNode / attachmentAnchorNodeToMd', () => {
|
|
113
|
-
const node = {
|
|
114
|
-
nodeName: 'A',
|
|
115
|
-
getAttribute: (name) => ({
|
|
116
|
-
'data-is-tapd-attachment': 'true',
|
|
117
|
-
'data-name': 'comment-embed.txt',
|
|
118
|
-
'data-size': '54',
|
|
119
|
-
'data-file-type': 'text',
|
|
120
|
-
href: `/${WS}/attachments/preview_attachments/1139814312001000004/story_description_attachment`,
|
|
121
|
-
})[name] ?? null,
|
|
122
|
-
};
|
|
123
|
-
assert.equal(isAttachmentAnchorNode(node), true);
|
|
124
|
-
assert.equal(attachmentAnchorNodeToMd(node), `[📎 comment-embed.txt](attach:${WS}/1139814312001000004)`);
|
|
125
|
-
const bare = { nodeName: 'A', getAttribute: () => null };
|
|
126
|
-
assert.equal(isAttachmentAnchorNode(bare), false);
|
|
127
|
-
assert.equal(attachmentAnchorNodeToMd(bare), null);
|
|
128
|
-
}],
|
|
129
|
-
];
|
|
130
|
-
let failed = 0;
|
|
131
|
-
for (const [name, check] of checks) {
|
|
132
|
-
try {
|
|
133
|
-
check();
|
|
134
|
-
console.log(`PASS ${name}`);
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
failed += 1;
|
|
138
|
-
console.error(`FAIL ${name}`);
|
|
139
|
-
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (failed > 0) {
|
|
143
|
-
console.error(`\n${failed}/${checks.length} failed`);
|
|
144
|
-
process.exitCode = 1;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
console.log(`\n${checks.length}/${checks.length} passed`);
|
|
148
|
-
}
|
|
149
|
-
//# sourceMappingURL=attachment-anchor.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-anchor.test.js","sourceRoot":"","sources":["../../src/richtext/attachment-anchor.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/** HTML 文本节点转义(& < >)。 */
|
|
2
|
-
export declare function escapeHtmlText(value: string): string;
|
|
3
|
-
/** HTML 属性值转义(& < > " ')。 */
|
|
4
|
-
export declare function escapeHtmlAttr(value: string): string;
|
|
5
|
-
/** 反解 TAPD 富文本属性值中可能出现的实体(常用命名实体 + 十进制/十六进制数字实体)。未识别的实体原样保留。 */
|
|
6
|
-
export declare function decodeHtmlEntities(value: string): string;
|
|
7
|
-
//# sourceMappingURL=escape.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/richtext/escape.ts"],"names":[],"mappings":"AAeA,0BAA0B;AAC1B,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,6BAA6B;AAC7B,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAWD,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOxD"}
|
package/dist/richtext/escape.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// richtext 语义库内部工具:最小 HTML 转义/反解(零依赖、纯函数)。
|
|
2
|
-
// 只覆盖 TAPD 富文本锚点/at-who 生成与解析所需的最小集合,不做完整 HTML 实体表。
|
|
3
|
-
const TEXT_ESCAPES = {
|
|
4
|
-
'&': '&',
|
|
5
|
-
'<': '<',
|
|
6
|
-
'>': '>',
|
|
7
|
-
};
|
|
8
|
-
const ATTR_ESCAPES = {
|
|
9
|
-
...TEXT_ESCAPES,
|
|
10
|
-
'"': '"',
|
|
11
|
-
"'": ''',
|
|
12
|
-
};
|
|
13
|
-
/** HTML 文本节点转义(& < >)。 */
|
|
14
|
-
export function escapeHtmlText(value) {
|
|
15
|
-
return value.replace(/[&<>]/g, (ch) => TEXT_ESCAPES[ch] ?? ch);
|
|
16
|
-
}
|
|
17
|
-
/** HTML 属性值转义(& < > " ')。 */
|
|
18
|
-
export function escapeHtmlAttr(value) {
|
|
19
|
-
return value.replace(/[&<>"']/g, (ch) => ATTR_ESCAPES[ch] ?? ch);
|
|
20
|
-
}
|
|
21
|
-
const NAMED_ENTITIES = {
|
|
22
|
-
amp: '&',
|
|
23
|
-
lt: '<',
|
|
24
|
-
gt: '>',
|
|
25
|
-
quot: '"',
|
|
26
|
-
apos: "'",
|
|
27
|
-
nbsp: ' ',
|
|
28
|
-
};
|
|
29
|
-
/** 反解 TAPD 富文本属性值中可能出现的实体(常用命名实体 + 十进制/十六进制数字实体)。未识别的实体原样保留。 */
|
|
30
|
-
export function decodeHtmlEntities(value) {
|
|
31
|
-
return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, body) => {
|
|
32
|
-
const codePoint = numericEntityCodePoint(body);
|
|
33
|
-
if (codePoint !== null)
|
|
34
|
-
return safeFromCodePoint(codePoint, whole);
|
|
35
|
-
const named = NAMED_ENTITIES[body.toLowerCase()];
|
|
36
|
-
return named ?? whole;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function numericEntityCodePoint(body) {
|
|
40
|
-
if (body.startsWith('#x') || body.startsWith('#X')) {
|
|
41
|
-
const code = Number.parseInt(body.slice(2), 16);
|
|
42
|
-
return Number.isFinite(code) ? code : null;
|
|
43
|
-
}
|
|
44
|
-
if (body.startsWith('#')) {
|
|
45
|
-
const code = Number.parseInt(body.slice(1), 10);
|
|
46
|
-
return Number.isFinite(code) ? code : null;
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
function safeFromCodePoint(code, fallback) {
|
|
51
|
-
if (!Number.isInteger(code) || code < 0 || code > 0x10ffff)
|
|
52
|
-
return fallback;
|
|
53
|
-
try {
|
|
54
|
-
return String.fromCodePoint(code);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return fallback;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=escape.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../../src/richtext/escape.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,oDAAoD;AAEpD,MAAM,YAAY,GAAqC;IACrD,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;CACZ,CAAC;AAEF,MAAM,YAAY,GAAqC;IACrD,GAAG,YAAY;IACf,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,OAAO;CACb,CAAC;AAEF,0BAA0B;AAC1B,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,6BAA6B;AAC7B,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,cAAc,GAAqC;IACvD,GAAG,EAAE,GAAG;IACR,EAAE,EAAE,GAAG;IACP,EAAE,EAAE,GAAG;IACP,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;CACV,CAAC;AAEF,gEAAgE;AAChE,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,gCAAgC,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC7E,MAAM,SAAS,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,SAAS,KAAK,IAAI;YAAE,OAAO,iBAAiB,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACnE,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACjD,OAAO,KAAK,IAAI,KAAK,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAY;IAC1C,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IACvD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC5E,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/** at-who 标记里命中的一次提及。`[start, end)` 覆盖含 `@` 在内的整个原始标记区间。 */
|
|
2
|
-
export interface Mention {
|
|
3
|
-
name: string;
|
|
4
|
-
start: number;
|
|
5
|
-
end: number;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* 最小结构 DOM 节点视图(duck typing)——真实 turndown/DOM 节点天然满足,
|
|
9
|
-
* 测试可直接传字面量对象,保持本库零 DOM 依赖。
|
|
10
|
-
*/
|
|
11
|
-
export interface HtmlNodeLike {
|
|
12
|
-
nodeName: string;
|
|
13
|
-
getAttribute(name: string): string | null;
|
|
14
|
-
readonly textContent?: string | null;
|
|
15
|
-
}
|
|
16
|
-
/** 生成 at-who 标记(昵称含 HTML 特殊字符时按属性/文本位分别转义)。 */
|
|
17
|
-
export declare function renderMentionHtml(name: string): string;
|
|
18
|
-
/** 扫描文本中全部合法 mention(跳过 @@ 转义)。 */
|
|
19
|
-
export declare function findMentions(text: string): Mention[];
|
|
20
|
-
/**
|
|
21
|
-
* md 文本 → html 语义片段:`@@` → 字面 `@`,合法 `@昵称` → at-who 标记;
|
|
22
|
-
* 其余内容(含疑似 html 标签区域)原样透传,交给基础转换器(markdown-it)完成。
|
|
23
|
-
* 注意:本函数不做代码段/链接 URL 等 md 语法识别(见文件头分层约定)。
|
|
24
|
-
*/
|
|
25
|
-
export declare function mentionMdToHtml(md: string): string;
|
|
26
|
-
/**
|
|
27
|
-
* html 文本 → md 文本:会把命中 mention 规则的字面 `@名字` 加倍为 `@@名字`、
|
|
28
|
-
* `@@` 字面对加倍为 `@@@@`,使 md→html 重转换后语义不变(round-trip 稳定);
|
|
29
|
-
* 不命中规则的 `@`(如邮箱、工单号)原样保留,避免无谓转义噪音。
|
|
30
|
-
* 与 md→html 的 scanAtTokens 共用同一套规则,双向严格镜像。
|
|
31
|
-
*/
|
|
32
|
-
export declare function escapeAtLiteralsForMd(text: string): string;
|
|
33
|
-
/** 判断一个标签串是否为 at-who 标记(`<b>` 且 class 含 at-who)。 */
|
|
34
|
-
export declare function isAtWhoTag(tagHtml: string): boolean;
|
|
35
|
-
/**
|
|
36
|
-
* html 片段 → md 语义片段:
|
|
37
|
-
* - at-who 标记 → `@昵称`(data-userid 缺失时回退锚点内文本;禁止降级为 `**粗体**`)
|
|
38
|
-
* - 其余标签原样透传(非 at-who 的 `<b>` 不动,交给基础转换器 turndown 处理)
|
|
39
|
-
* - 标签之间的文本按 escapeAtLiteralsForMd 规则保护字面 @
|
|
40
|
-
*/
|
|
41
|
-
export declare function mentionHtmlToMd(html: string): string;
|
|
42
|
-
/** turndown 规则 filter 用:节点是否为 at-who 标记。 */
|
|
43
|
-
export declare function isAtWhoNode(node: HtmlNodeLike): boolean;
|
|
44
|
-
/**
|
|
45
|
-
* turndown 规则 replacement 用:at-who 节点 → `@昵称`。
|
|
46
|
-
* data-userid 优先;缺失时回退节点文本(去首 @)。永不产出 `**`。
|
|
47
|
-
*/
|
|
48
|
-
export declare function atWhoNodeToMd(node: HtmlNodeLike): string;
|
|
49
|
-
//# sourceMappingURL=mention.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mention.d.ts","sourceRoot":"","sources":["../../src/richtext/mention.ts"],"names":[],"mappings":"AAyBA,4DAA4D;AAC5D,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAQD,+CAA+C;AAC/C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMtD;AAsCD,mCAAmC;AACnC,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAIpD;AAgBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS1D;AAYD,oDAAoD;AACpD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGnD;AASD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcpD;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAGvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAKxD"}
|
package/dist/richtext/mention.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
// TAPD 2.0 rc.2 P3(需求 1139814312001001548)—— @昵称 ↔ at-who 标记双向语义(纯函数层)。
|
|
2
|
-
// 设计依据:FSD §3.5 / §8;e2e 金样 test/e2e-rc2/poc-rc2-mention.mjs(保真实证)。
|
|
3
|
-
//
|
|
4
|
-
// HTML 形态(逐字段定稿,属性顺序勿改——data-userid 存昵称而非数字 uid):
|
|
5
|
-
// <b class="at-who" contenteditable="false" data-userid="徐昭" data-type="user">@徐昭</b>
|
|
6
|
-
//
|
|
7
|
-
// md 形态:`@昵称`;转义语法:`@@` → 字面 `@`(不触发 mention)。
|
|
8
|
-
//
|
|
9
|
-
// 昵称文法(FSD §8 误伤对策:仅匹配「@ + 中文名/英文名 + 边界」):
|
|
10
|
-
// name-char ::= Han 字符 | [A-Za-z0-9_]
|
|
11
|
-
// mention ::= left-boundary '@' name-char{1,}
|
|
12
|
-
// left-boundary ::= 文本起始 | 非 name-char
|
|
13
|
-
// 名字 greedily 吃满 name-char,天然获得右边界(空白/标点/行尾)。
|
|
14
|
-
// 左边界要求 @ 前不能紧跟 name-char —— 邮箱 user@example.com、工单号 T-100@QA 因此不命中;
|
|
15
|
-
// 代价是中文紧贴写法(联系@徐昭)不生成 at-who,但 R4 实证服务端对纯文本 @昵称 仍智能识别,欠标记无害,
|
|
16
|
-
// 过标记(把邮箱拆成 at-who)才有害,故取保守侧。
|
|
17
|
-
//
|
|
18
|
-
// 分层约定(Wave 2 接线必读):
|
|
19
|
-
// - 本文件是纯函数语义层,零 IO、零 SDK 依赖,不感知 md 代码段/链接 URL 等 md 语法;
|
|
20
|
-
// `@` 出现在代码段或 URL 内的场景必须在 markdown-it/turndown 的 token 级接线时规避
|
|
21
|
-
// (本层提供的 fragment 级函数只做语义 token 替换,其余内容原样透传)。
|
|
22
|
-
// - html→md 方向:at-who 一律还原为 `@昵称`,禁止降级为 `**粗体**`(R7 实测破坏点)。
|
|
23
|
-
import { decodeHtmlEntities, escapeHtmlAttr, escapeHtmlText } from './escape.js';
|
|
24
|
-
const NAME_CHAR_RE = /[\p{Script=Han}A-Za-z0-9_]/u;
|
|
25
|
-
function isNameChar(ch) {
|
|
26
|
-
return ch !== undefined && NAME_CHAR_RE.test(ch);
|
|
27
|
-
}
|
|
28
|
-
/** 生成 at-who 标记(昵称含 HTML 特殊字符时按属性/文本位分别转义)。 */
|
|
29
|
-
export function renderMentionHtml(name) {
|
|
30
|
-
if (!name)
|
|
31
|
-
throw new TypeError('mention name must be a non-empty string');
|
|
32
|
-
return (`<b class="at-who" contenteditable="false" data-userid="${escapeHtmlAttr(name)}" data-type="user">` +
|
|
33
|
-
`@${escapeHtmlText(name)}</b>`);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* 单遍扫描文本中的 @ 语义 token:
|
|
37
|
-
* - `@@` 相邻对 → escape(md 侧表示一个字面 @)
|
|
38
|
-
* - 通过左边界 + 名字规则的 `@名字` → mention
|
|
39
|
-
* - 其余 `@` 不产出 token(由调用方原样处理)
|
|
40
|
-
* 供 md→html 与 html→md 两个方向共用,保证双向规则严格镜像。
|
|
41
|
-
*/
|
|
42
|
-
function scanAtTokens(text) {
|
|
43
|
-
const tokens = [];
|
|
44
|
-
let i = 0;
|
|
45
|
-
while (i < text.length) {
|
|
46
|
-
if (text[i] !== '@') {
|
|
47
|
-
i += 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (text[i + 1] === '@') {
|
|
51
|
-
tokens.push({ kind: 'escape', start: i, end: i + 2 });
|
|
52
|
-
i += 2;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (i > 0 && isNameChar(text[i - 1])) {
|
|
56
|
-
i += 1;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
let j = i + 1;
|
|
60
|
-
while (j < text.length && isNameChar(text[j]))
|
|
61
|
-
j += 1;
|
|
62
|
-
if (j > i + 1)
|
|
63
|
-
tokens.push({ kind: 'mention', name: text.slice(i + 1, j), start: i, end: j });
|
|
64
|
-
i = Math.max(j, i + 1);
|
|
65
|
-
}
|
|
66
|
-
return tokens;
|
|
67
|
-
}
|
|
68
|
-
/** 扫描文本中全部合法 mention(跳过 @@ 转义)。 */
|
|
69
|
-
export function findMentions(text) {
|
|
70
|
-
return scanAtTokens(text).flatMap((token) => token.kind === 'mention' ? [{ name: token.name, start: token.start, end: token.end }] : []);
|
|
71
|
-
}
|
|
72
|
-
/** md 文本片段中「疑似 html 标签」的区域:@ 语义跳过、原样透传(fragment 层不做 md 语法解析的边界)。 */
|
|
73
|
-
const TAG_RE = /<\/?[a-zA-Z][^>]*>/g;
|
|
74
|
-
function replaceAtSemantics(segment) {
|
|
75
|
-
let out = '';
|
|
76
|
-
let pos = 0;
|
|
77
|
-
for (const token of scanAtTokens(segment)) {
|
|
78
|
-
out += segment.slice(pos, token.start);
|
|
79
|
-
out += token.kind === 'escape' ? '@' : renderMentionHtml(token.name);
|
|
80
|
-
pos = token.end;
|
|
81
|
-
}
|
|
82
|
-
return out + segment.slice(pos);
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* md 文本 → html 语义片段:`@@` → 字面 `@`,合法 `@昵称` → at-who 标记;
|
|
86
|
-
* 其余内容(含疑似 html 标签区域)原样透传,交给基础转换器(markdown-it)完成。
|
|
87
|
-
* 注意:本函数不做代码段/链接 URL 等 md 语法识别(见文件头分层约定)。
|
|
88
|
-
*/
|
|
89
|
-
export function mentionMdToHtml(md) {
|
|
90
|
-
let out = '';
|
|
91
|
-
let pos = 0;
|
|
92
|
-
TAG_RE.lastIndex = 0;
|
|
93
|
-
for (;;) {
|
|
94
|
-
const match = TAG_RE.exec(md);
|
|
95
|
-
const segmentEnd = match ? match.index : md.length;
|
|
96
|
-
out += replaceAtSemantics(md.slice(pos, segmentEnd));
|
|
97
|
-
if (!match)
|
|
98
|
-
break;
|
|
99
|
-
out += match[0];
|
|
100
|
-
pos = segmentEnd + match[0].length;
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* html 文本 → md 文本:会把命中 mention 规则的字面 `@名字` 加倍为 `@@名字`、
|
|
106
|
-
* `@@` 字面对加倍为 `@@@@`,使 md→html 重转换后语义不变(round-trip 稳定);
|
|
107
|
-
* 不命中规则的 `@`(如邮箱、工单号)原样保留,避免无谓转义噪音。
|
|
108
|
-
* 与 md→html 的 scanAtTokens 共用同一套规则,双向严格镜像。
|
|
109
|
-
*/
|
|
110
|
-
export function escapeAtLiteralsForMd(text) {
|
|
111
|
-
let out = '';
|
|
112
|
-
let pos = 0;
|
|
113
|
-
for (const token of scanAtTokens(text)) {
|
|
114
|
-
out += text.slice(pos, token.start);
|
|
115
|
-
out += token.kind === 'escape' ? '@@@@' : `@@${token.name}`;
|
|
116
|
-
pos = token.end;
|
|
117
|
-
}
|
|
118
|
-
return out + text.slice(pos);
|
|
119
|
-
}
|
|
120
|
-
const B_TAG_RE = /<b\b[^>]*>([\s\S]*?)<\/b\s*>|<\/?[a-zA-Z][^>]*>/gi;
|
|
121
|
-
const CLASS_ATTR_RE = /\bclass\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
122
|
-
const USERID_ATTR_RE = /\bdata-userid\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
123
|
-
function classListOf(openTag) {
|
|
124
|
-
const match = CLASS_ATTR_RE.exec(openTag);
|
|
125
|
-
if (!match)
|
|
126
|
-
return [];
|
|
127
|
-
return (match[1] ?? match[2] ?? '').split(/\s+/);
|
|
128
|
-
}
|
|
129
|
-
/** 判断一个标签串是否为 at-who 标记(`<b>` 且 class 含 at-who)。 */
|
|
130
|
-
export function isAtWhoTag(tagHtml) {
|
|
131
|
-
const openMatch = /^<b\b[^>]*>/i.exec(tagHtml);
|
|
132
|
-
return openMatch !== null && classListOf(openMatch[0]).includes('at-who');
|
|
133
|
-
}
|
|
134
|
-
function nicknameFromTag(tagHtml, innerHtml) {
|
|
135
|
-
const useridMatch = USERID_ATTR_RE.exec(tagHtml);
|
|
136
|
-
if (useridMatch)
|
|
137
|
-
return decodeHtmlEntities(useridMatch[1] ?? useridMatch[2] ?? '');
|
|
138
|
-
const text = decodeHtmlEntities((innerHtml ?? '').replace(/<[^>]+>/g, '')).trim();
|
|
139
|
-
return text.startsWith('@') ? text.slice(1) : text;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* html 片段 → md 语义片段:
|
|
143
|
-
* - at-who 标记 → `@昵称`(data-userid 缺失时回退锚点内文本;禁止降级为 `**粗体**`)
|
|
144
|
-
* - 其余标签原样透传(非 at-who 的 `<b>` 不动,交给基础转换器 turndown 处理)
|
|
145
|
-
* - 标签之间的文本按 escapeAtLiteralsForMd 规则保护字面 @
|
|
146
|
-
*/
|
|
147
|
-
export function mentionHtmlToMd(html) {
|
|
148
|
-
let out = '';
|
|
149
|
-
let pos = 0;
|
|
150
|
-
let match;
|
|
151
|
-
B_TAG_RE.lastIndex = 0;
|
|
152
|
-
while ((match = B_TAG_RE.exec(html)) !== null) {
|
|
153
|
-
out += escapeAtLiteralsForMd(html.slice(pos, match.index));
|
|
154
|
-
const isAtWho = isAtWhoTag(match[0]);
|
|
155
|
-
const nickname = isAtWho ? nicknameFromTag(match[0], match[1]) : '';
|
|
156
|
-
out += nickname ? `@${nickname}` : match[0];
|
|
157
|
-
pos = match.index + match[0].length;
|
|
158
|
-
}
|
|
159
|
-
out += escapeAtLiteralsForMd(html.slice(pos));
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
162
|
-
/** turndown 规则 filter 用:节点是否为 at-who 标记。 */
|
|
163
|
-
export function isAtWhoNode(node) {
|
|
164
|
-
if (node.nodeName.toUpperCase() !== 'B')
|
|
165
|
-
return false;
|
|
166
|
-
return (node.getAttribute('class') ?? '').split(/\s+/).includes('at-who');
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* turndown 规则 replacement 用:at-who 节点 → `@昵称`。
|
|
170
|
-
* data-userid 优先;缺失时回退节点文本(去首 @)。永不产出 `**`。
|
|
171
|
-
*/
|
|
172
|
-
export function atWhoNodeToMd(node) {
|
|
173
|
-
const userid = node.getAttribute('data-userid');
|
|
174
|
-
if (userid)
|
|
175
|
-
return `@${userid}`;
|
|
176
|
-
const text = (node.textContent ?? '').trim();
|
|
177
|
-
return text.startsWith('@') ? text : `@${text}`;
|
|
178
|
-
}
|
|
179
|
-
//# sourceMappingURL=mention.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mention.js","sourceRoot":"","sources":["../../src/richtext/mention.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mention.test.d.ts","sourceRoot":"","sources":["../../src/richtext/mention.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// mention 语义单测:生成/解析/扫描/边界误伤(FSD §8)/双向镜像。tsx 直跑,不进 package.json test 脚本(接线归集成者)。
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { atWhoNodeToMd, escapeAtLiteralsForMd, findMentions, isAtWhoNode, isAtWhoTag, mentionHtmlToMd, mentionMdToHtml, renderMentionHtml, } from './mention.js';
|
|
4
|
-
const AT_WHO_XUZHAO = '<b class="at-who" contenteditable="false" data-userid="徐昭" data-type="user">@徐昭</b>';
|
|
5
|
-
const checks = [
|
|
6
|
-
['renderMentionHtml: 逐字段复刻 at-who 模板(e2e 金样)', () => {
|
|
7
|
-
assert.equal(renderMentionHtml('徐昭'), AT_WHO_XUZHAO);
|
|
8
|
-
}],
|
|
9
|
-
['renderMentionHtml: 昵称含 HTML 特殊字符分别转义 attr/文本位', () => {
|
|
10
|
-
const html = renderMentionHtml('a"b<c>');
|
|
11
|
-
assert.ok(html.includes('data-userid="a"b<c>"'));
|
|
12
|
-
assert.ok(html.endsWith('>@a"b<c></b>'));
|
|
13
|
-
assert.ok(html.startsWith('<b class="at-who" contenteditable="false" '));
|
|
14
|
-
}],
|
|
15
|
-
['renderMentionHtml: 空昵称抛 TypeError', () => {
|
|
16
|
-
assert.throws(() => renderMentionHtml(''), TypeError);
|
|
17
|
-
}],
|
|
18
|
-
['findMentions: 基本命中与区间索引', () => {
|
|
19
|
-
assert.deepEqual(findMentions('请 @徐昭 尽快确认'), [{ name: '徐昭', start: 2, end: 5 }]);
|
|
20
|
-
assert.deepEqual(findMentions('no mention here'), []);
|
|
21
|
-
}],
|
|
22
|
-
['findMentions: 右边界贪吃 + 多命中', () => {
|
|
23
|
-
assert.deepEqual(findMentions('@bob_dev.'), [{ name: 'bob_dev', start: 0, end: 8 }]);
|
|
24
|
-
assert.deepEqual(findMentions('@徐昭,你好'), [{ name: '徐昭', start: 0, end: 3 }]);
|
|
25
|
-
assert.deepEqual(findMentions('找 @张三 和 @Alice_1 一起'), [
|
|
26
|
-
{ name: '张三', start: 2, end: 5 },
|
|
27
|
-
{ name: 'Alice_1', start: 8, end: 16 },
|
|
28
|
-
]);
|
|
29
|
-
}],
|
|
30
|
-
['findMentions: FSD §8 误伤对策——邮箱/工单号/紧贴中文不命中', () => {
|
|
31
|
-
assert.deepEqual(findMentions('联系 user@example.com 获取'), []);
|
|
32
|
-
assert.deepEqual(findMentions('工单号 T-100@QA 由 support@test 处理'), []);
|
|
33
|
-
assert.deepEqual(findMentions('联系@徐昭 谢谢'), []); // @ 前紧贴中文(保守侧不标记;R4 服务端仍识别)
|
|
34
|
-
assert.deepEqual(findMentions('版本 v1.2@release'), []);
|
|
35
|
-
}],
|
|
36
|
-
['findMentions: @@ 转义与孤立 @ 不命中', () => {
|
|
37
|
-
assert.deepEqual(findMentions('@@徐昭'), []);
|
|
38
|
-
assert.deepEqual(findMentions('行尾单独 @ 保持'), []);
|
|
39
|
-
assert.deepEqual(findMentions('@'), []);
|
|
40
|
-
assert.deepEqual(findMentions('@ 上路'), []);
|
|
41
|
-
}],
|
|
42
|
-
['findMentions: 数字昵称与起始边界', () => {
|
|
43
|
-
assert.deepEqual(findMentions('@123 上线'), [{ name: '123', start: 0, end: 4 }]);
|
|
44
|
-
assert.deepEqual(findMentions('( @张三 )'), [{ name: '张三', start: 2, end: 5 }]);
|
|
45
|
-
}],
|
|
46
|
-
['mentionMdToHtml: 昵称 → at-who,其余原样', () => {
|
|
47
|
-
assert.equal(mentionMdToHtml('请 @徐昭 今天内确认'), `请 ${AT_WHO_XUZHAO} 今天内确认`);
|
|
48
|
-
}],
|
|
49
|
-
['mentionMdToHtml: @@ → 字面 @ 不触发 mention;@@@ 组合行为', () => {
|
|
50
|
-
assert.equal(mentionMdToHtml('@@徐昭'), '@徐昭');
|
|
51
|
-
assert.equal(mentionMdToHtml('@@@徐昭'), `@${AT_WHO_XUZHAO}`);
|
|
52
|
-
assert.equal(mentionMdToHtml('@@@@'), '@@');
|
|
53
|
-
}],
|
|
54
|
-
['mentionMdToHtml: 疑似 html 标签区域 @ 语义跳过(透传)', () => {
|
|
55
|
-
assert.equal(mentionMdToHtml('a <b>x@y</b> @徐昭'), `a <b>x@y</b> ${AT_WHO_XUZHAO}`);
|
|
56
|
-
}],
|
|
57
|
-
['mentionHtmlToMd: at-who → @昵称,禁止降级 **粗体**(R7 回归)', () => {
|
|
58
|
-
const md = mentionHtmlToMd(`<p>${AT_WHO_XUZHAO} 请查收</p>`);
|
|
59
|
-
assert.equal(md, '<p>@徐昭 请查收</p>');
|
|
60
|
-
assert.ok(!md.includes('**'));
|
|
61
|
-
}],
|
|
62
|
-
['mentionHtmlToMd: 非 at-who 的 <b> 原样(不吞不转)', () => {
|
|
63
|
-
assert.equal(mentionHtmlToMd('<b>user@x</b>'), '<b>user@x</b>');
|
|
64
|
-
}],
|
|
65
|
-
['mentionHtmlToMd: 文本字面 @ 按 mention 规则保护为 @@;不命中者原样', () => {
|
|
66
|
-
assert.equal(mentionHtmlToMd('<p>@x user@y.com @@z</p>'), '<p>@@x user@y.com @@@@z</p>');
|
|
67
|
-
}],
|
|
68
|
-
['escapeAtLiteralsForMd: 与 md 方向规则严格镜像', () => {
|
|
69
|
-
assert.equal(escapeAtLiteralsForMd('@徐昭'), '@@徐昭');
|
|
70
|
-
assert.equal(escapeAtLiteralsForMd('user@example.com'), 'user@example.com');
|
|
71
|
-
assert.equal(escapeAtLiteralsForMd('@@'), '@@@@');
|
|
72
|
-
}],
|
|
73
|
-
['isAtWhoTag / 节点辅助: class 识别与 data-userid 优先', () => {
|
|
74
|
-
assert.equal(isAtWhoTag(AT_WHO_XUZHAO), true);
|
|
75
|
-
assert.equal(isAtWhoTag('<b class="tapd-editor__bold">@徐昭</b>'), false);
|
|
76
|
-
const node = {
|
|
77
|
-
nodeName: 'B',
|
|
78
|
-
getAttribute: (name) => (name === 'class' ? 'at-who' : name === 'data-userid' ? '徐昭' : null),
|
|
79
|
-
};
|
|
80
|
-
assert.equal(isAtWhoNode(node), true);
|
|
81
|
-
assert.equal(atWhoNodeToMd(node), '@徐昭');
|
|
82
|
-
}],
|
|
83
|
-
['节点辅助: data-userid 缺失回退文本', () => {
|
|
84
|
-
const node = {
|
|
85
|
-
nodeName: 'b',
|
|
86
|
-
getAttribute: (name) => (name === 'class' ? 'at-who' : null),
|
|
87
|
-
textContent: '@李四',
|
|
88
|
-
};
|
|
89
|
-
assert.equal(isAtWhoNode(node), true);
|
|
90
|
-
assert.equal(atWhoNodeToMd(node), '@李四');
|
|
91
|
-
}],
|
|
92
|
-
['mention 双向镜像: md→html→md 恒等(多边界样本)', () => {
|
|
93
|
-
const samples = [
|
|
94
|
-
'请 @徐昭 今天内确认,抄送 @bob_dev。',
|
|
95
|
-
'@张三',
|
|
96
|
-
'user@example.com 与 T-100@QA 与 @@李四 与 @',
|
|
97
|
-
'(@王五)结束。',
|
|
98
|
-
];
|
|
99
|
-
for (const md of samples) {
|
|
100
|
-
assert.equal(mentionHtmlToMd(mentionMdToHtml(md)), md);
|
|
101
|
-
}
|
|
102
|
-
}],
|
|
103
|
-
];
|
|
104
|
-
let failed = 0;
|
|
105
|
-
for (const [name, check] of checks) {
|
|
106
|
-
try {
|
|
107
|
-
check();
|
|
108
|
-
console.log(`PASS ${name}`);
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
failed += 1;
|
|
112
|
-
console.error(`FAIL ${name}`);
|
|
113
|
-
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (failed > 0) {
|
|
117
|
-
console.error(`\n${failed}/${checks.length} failed`);
|
|
118
|
-
process.exitCode = 1;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
console.log(`\n${checks.length}/${checks.length} passed`);
|
|
122
|
-
}
|
|
123
|
-
//# sourceMappingURL=mention.test.js.map
|