lke-component 1.2.22 → 1.2.23

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.
@@ -0,0 +1,935 @@
1
+ import { getDefaultWhiteList, escapeAttrValue, whiteList } from 'xss';
2
+
3
+ window.isMathJaxLoaded = false;
4
+ /**
5
+ * Mathjax to be injected into the document head
6
+ * @param {IInjectOptions} options The script attribute
7
+ * @returns void
8
+ */
9
+ export function injectMathJax({ loadType, url }) {
10
+ console.log('injectMathJax 3');
11
+ if (!window.isMathJaxLoaded) {
12
+ const script = document.createElement('script');
13
+ script.src = window.mathjaxSrc ? window.mathjaxSrc :
14
+ 'https://cdn.xiaowei.qq.com/webim/assets/static/mathjax/es5/tex-chtml.min.es5.js';
15
+ script.async = true;
16
+ document.head.appendChild(script);
17
+ }
18
+ }
19
+ // const tmpArr = [];
20
+ /**
21
+ * Global configuration MathJax
22
+ * @param {IMathJaxProps} options Custom MathJax global configuration reference: http://docs.mathjax.org/en/latest/
23
+ * @param {FnType} callback Mathjax loading is completed, you need to perform the function
24
+ */
25
+ export function initMathJax(
26
+ options = {},
27
+ callback,
28
+ ) {
29
+ // console.log('initMathJax 1', window.isMathJaxLoaded);
30
+ if (!window.isMathJaxLoaded) {
31
+ // console.log('initMathJax 2', window.MathJax);
32
+ if (window.MathJax) {
33
+ const tmp = window.MathJax.startup.pageReady;
34
+ window.MathJax.startup.pageReady = () => {
35
+ console.log('MathJax callback');
36
+ callback && callback();
37
+ window.isMathJaxLoaded = true;
38
+ tmp();
39
+ // const fn = tmpArr.shift();
40
+ // fn && fn();
41
+ }
42
+ // tmpArr.push(tmp);
43
+ } else {
44
+ injectMathJax({});
45
+ window.MathJax = {
46
+ loader: {load: ['[tex]/mhchem']},
47
+ tex: {
48
+ inlineMath: [
49
+ ["$", "$"],
50
+ ["\\(", "\\)"]
51
+ ], //行内公式选择符
52
+ displayMath: [
53
+ ["$$", "$$"],
54
+ ["\\[", "\\]"]
55
+ ], //段内公式选择符
56
+ // 下面两个主要是支持渲染某些公式,可以自己了解
57
+ processEnvironments: true,
58
+ processRefs: true,
59
+ processEscapes: true,
60
+ packages: {'[+]': ['mhchem']},
61
+ equationNumbers: {autoNumber: "AMS"},
62
+ },
63
+ options: {
64
+ // 跳过渲染的标签
65
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
66
+ // 跳过mathjax处理的元素的类名,任何元素指定一个类 tex2jax_ignore 将被跳过,多个累=类名'class1|class2'
67
+ ignoreHtmlClass: 'tex2jax_ignore',
68
+ },
69
+ startup: {
70
+ // 当mathjax加载并初始化完成后的回调
71
+ pageReady: () => {
72
+ console.log('initMathJax 4 pageReady');
73
+ callback && callback();
74
+ window.isMathJaxLoaded = true;
75
+ },
76
+ },
77
+ svg: {
78
+ fontCache: 'global',
79
+ },
80
+ };
81
+ }
82
+ } else {
83
+ callback && callback();
84
+ }
85
+ // window.MathJax.Hub.Config({
86
+ // extensions: ["tex2jax.js"],
87
+ // showProcessingMessages: false, //关闭js加载过程信息
88
+ // messageStyle: "none", //不显示信息
89
+ // jax: ["input/TeX", "output/HTML-CSS"],
90
+ // tex2jax: {
91
+ // inlineMath: [
92
+ // ["$", "$"],
93
+ // ["\\(", "\\)"]
94
+ // ], //行内公式选择符
95
+ // displayMath: [
96
+ // ["$$", "$$"],
97
+ // ["\\[", "\\]"]
98
+ // ], //段内公式选择符
99
+ // skipTags: ["script", "noscript", "style", "textarea", "pre", "code", "a"] //避开某些标签
100
+ // },
101
+ // "HTML-CSS": {
102
+ // availableFonts: ["STIX", "TeX"], //可选字体
103
+ // showMathMenu: false //关闭右击菜单显示
104
+ // }
105
+ // });
106
+ // isMathjaxConfig = true; //配置完成,改为true
107
+ }
108
+
109
+ /**
110
+ * Manual rendering formula, returns a Promise
111
+ * @param {HTMLElement} el Need to be mathjax rendered HTMLElement
112
+ * @returns Promise
113
+ */
114
+ export function renderByMathjax(el) {
115
+ // console.log('renderByMathjax', window.MathJax ? window.MathJax.version : '');
116
+
117
+ if (el && !Array.isArray(el)) {
118
+ el = [el]
119
+ }
120
+
121
+ return new Promise((resolve, reject) => {
122
+ initMathJax({},() => {
123
+ window.MathJax.typesetPromise(el)
124
+ .then(() => {
125
+ resolve(void 0)
126
+ })
127
+ .catch((err) => reject(err))
128
+ });
129
+ });
130
+ }
131
+
132
+ export function partialDescape(html) {
133
+ var lines = html.split('\n');
134
+ var out = '';
135
+
136
+ // is true when we are
137
+ // ```
138
+ // inside a code block
139
+ // ```
140
+ var inside_code = false;
141
+
142
+ for (var i = 0; i < lines.length; i++) {
143
+ // a hack to properly rendre the blockquotes
144
+ if (lines[i].startsWith('&gt;')) {
145
+ lines[i] = lines[i].replace(/&gt;/g, '>');
146
+ }
147
+
148
+ // rendrer properly stuff like this
149
+ // ```c
150
+ // if (a > b)
151
+ // ```
152
+ if (inside_code) {
153
+ // inside the code we descape stuff
154
+ lines[i] = lines[i]
155
+ .replace(new RegExp('&lt;', 'g'), '<')
156
+ .replace(new RegExp('&gt;', 'g'), '>')
157
+ .replace(new RegExp('&quot;', 'g'), '"')
158
+ .replace(new RegExp('&#39;', 'g'), '\'');
159
+ }
160
+ if (lines[i].startsWith('```')) {
161
+ inside_code = ! inside_code;
162
+ }
163
+ out += lines[i] + '\n';
164
+ }
165
+ return out.replace(/<span>\\<\/span>/g, '\\\\');
166
+ }
167
+
168
+ export function escapeStr (html, encode) {
169
+ const regex = new RegExp("&(?!#?\\w+;)", "g");
170
+ if (html && html.replace) {
171
+ return html
172
+ .replace(!encode ? regex : new RegExp('&', 'g'), '&amp;')
173
+ .replace(new RegExp('<', 'g'), '&lt;')
174
+ .replace(new RegExp('>', 'g'), '&gt;')
175
+ .replace(new RegExp('"', 'g'), '&quot;')
176
+ .replace(new RegExp("'", 'g'), '&#39;');
177
+ }
178
+ return html;
179
+ }
180
+ export function revertEscapeStr(escapedHtml) {
181
+ if (escapedHtml && escapedHtml.replace) {
182
+ return escapedHtml
183
+ .replace(new RegExp('&quot;', 'g'), '"')
184
+ .replace(new RegExp('&#39;', 'g'), "'")
185
+ .replace(new RegExp('&lt;', 'g'), '<')
186
+ .replace(new RegExp('&gt;', 'g'), '>')
187
+ .replace(new RegExp('&amp;', 'g'), '&')
188
+ .replace(new RegExp('<span>\\\\<\\/span>', 'g'), '\\\\');
189
+ }
190
+ return escapedHtml;
191
+ }
192
+
193
+ export function extractCodeBlocks(mdString) {
194
+ // 用于保存代码块的数组
195
+ const codeBlocks = [];
196
+
197
+ // 定义匹配代码块的正则表达式
198
+ const codeBlockPattern = new RegExp('```[\\s\\S]*?```', 'g');
199
+
200
+ // 替换代码块为特殊字符串,并保存代码块内容
201
+ const modifiedMdString = mdString.replace(codeBlockPattern, (match) => {
202
+ codeBlocks.push(match);
203
+ return '{{CODE_BLOCK}}';
204
+ });
205
+
206
+ return { modifiedMdString, codeBlocks };
207
+ }
208
+
209
+ export function restoreCodeBlocks(modifiedMdString, codeBlocks) {
210
+ // 定义特殊字符串的正则表达式
211
+ const specialStringPattern = new RegExp('\\{\\{CODE_BLOCK\\}\\}', 'g');
212
+
213
+ // 逐个替换特殊字符串为原始代码块
214
+ let index = 0;
215
+ const restoredMdString = modifiedMdString.replace(specialStringPattern, () => {
216
+ return codeBlocks[index++];
217
+ });
218
+
219
+ return restoredMdString;
220
+ }
221
+
222
+
223
+ export function replaceVideoTags(htmlString) {
224
+ const videoTagRegex = new RegExp('<video[\\s\\S]*?<\\/video>', 'gi');
225
+ const videoTags = [];
226
+ const replacedString = htmlString.replace(videoTagRegex, (match) => {
227
+ // 压缩 video 标签内部的换行和多余空白为单行,防止 markdown 渲染器插入 <br>/<p> 破坏 DOM 结构
228
+ const compacted = match.replace(/>\s+</g, '><').replace(/\n\s*/g, ' ').trim();
229
+ videoTags.push(compacted);
230
+ return 'HTML@@Video';
231
+ });
232
+ return { replacedString, videoTags };
233
+ }
234
+ export function restoreVideoTags(replacedString, videoTags) {
235
+ let restoredString = replacedString;
236
+ videoTags.forEach((videoTag) => {
237
+ restoredString = restoredString.replace('HTML@@Video', videoTag);
238
+ });
239
+ return restoredString;
240
+ }
241
+
242
+ function isFinancialData(text) {
243
+ let str = '$'+text+'$'
244
+ console.log('isMathJaxFormula 66', str);
245
+
246
+ // 因为需要结合语义,所以要把截取的$$之间的内容,前后补充$,在通过下面正则校验语义
247
+ const patterns = [
248
+ // 金额 + 变化关键词 + 百分比等字符
249
+ /\$\d+(\.\d+)?.*(change|up|down|gain|loss|move).*([\+\-]?\d+(\.\d+)?).*([\+\-]?\d+(\.\d+)?%)/i,
250
+ // 市值关键词 + is/:
251
+ /(market\s+cap|capitalization|market\s+value).*(is|\:)\s*\$\d+/i,
252
+ // 交易关键词 + at|\@ + 金额
253
+ /(trading|volume|shares|stock).*(at|\@)\s*\$\d+/i,
254
+ // 价格范围 + (to|-|~) + 第二个金额
255
+ /\$\d+(\.\d+)?\s*(to|-|~)\s*\$\d+(\.\d+)?/i
256
+ ];
257
+
258
+ return patterns.some(pattern => pattern.test(str));
259
+ }
260
+
261
+ /**
262
+ * 检测文本是否为数学公式
263
+ * @param {string} text 需要检测的文本
264
+ * @return {boolean} 是否为数学公式
265
+ */
266
+ export function isMathJaxFormula(text) {
267
+ console.log('isMathJaxFormula 6', isFinancialData(text), text);
268
+
269
+ // // 新增: 优先检测金融数据
270
+ if (isFinancialData(text)) {
271
+ return false;
272
+ }
273
+
274
+ // 新增: 检测 HTML 实体编码的 JSON 字符串
275
+ if (text.includes('\\&quot;')) {
276
+ // 检查是否是 JSON 对象或数组结构
277
+ if ((text.startsWith('{') && text.endsWith('}')) ||
278
+ (text.startsWith('[') && text.endsWith(']'))) {
279
+ return false;
280
+ }
281
+
282
+ // 检测典型的 JSON 键值对模式,但使用 HTML 实体编码的引号
283
+ const htmlEncodedJsonPattern = /\\&quot;[^\\]+\\&quot;\s*:\s*\\&quot;[^\\]*\\&quot;/;
284
+ if (htmlEncodedJsonPattern.test(text)) {
285
+ return false;
286
+ }
287
+ }
288
+
289
+ // 更一般化地检测各种编码的 JSON
290
+ if ((text.startsWith('{') && text.endsWith('}')) ||
291
+ (text.startsWith('[') && text.endsWith(']'))) {
292
+
293
+ // 检查是否包含各种形式的编码引号
294
+ if (text.includes('\\\"') || text.includes('\\"') ||
295
+ text.includes('\\&quot;') || text.includes('&quot;')) {
296
+
297
+ // 检查是否有冒号,这是 JSON 键值对的特征
298
+ if (text.includes(':')) {
299
+ return false;
300
+ }
301
+ }
302
+ }
303
+
304
+ // 原有函数的其余部分保持不变
305
+ // 首先排除 Markdown 特殊格式
306
+ // 1. 排除 Markdown 标题格式
307
+ if (/^#{1,6}\s+/.test(text.trim())) {
308
+ return false;
309
+ }
310
+
311
+ // 2. 排除包含多个 # 的解释性文本
312
+ if (text.includes('###') || text.includes('##') || /^#\s+/.test(text.trim())) {
313
+ return false;
314
+ }
315
+
316
+ // 如果文本为空或只是空白字符,则不是公式
317
+ if (!text || text.trim() === '') return false;
318
+
319
+ // 排除变量引用和非数学公式的内容
320
+ if (
321
+ text.match(/^CONST\[/) || // 排除 CONST[] 变量
322
+ text.match(/^[A-Z_]+\[/) || // 排除大写字母+下划线组成的变量引用
323
+ text.includes('@\\`') // 排除特定标记
324
+ ) {
325
+ return false;
326
+ }
327
+
328
+ // 新增: 排除代码引用/变量引用模式
329
+ // 匹配形如 $region\` 或包含反引号的代码引用格式
330
+ if (
331
+ (text.startsWith('$') && text.endsWith('$') && text.includes('\\`')) ||
332
+ /\$[a-zA-Z0-9_]+\\`/.test(text) ||
333
+ /`[^`]*`/.test(text)
334
+ ) {
335
+ return false;
336
+ }
337
+
338
+ // 新增: 排除带有 Markdown 语法的解释性文本
339
+ if (/\*\*.*?\*\*|__.*?__|_.*?_|\*.*?\*|~~.*?~~|\[.*?\]\(.*?\)/.test(text)) {
340
+ // 检查它是否确实像是解释性文本而不是数学公式
341
+ if (!/(\\frac|\\sum|\\int|\\prod|\\lim|\\{|\\}|\\[|\\]|\\(|\\))/.test(text)) {
342
+ return false;
343
+ }
344
+ }
345
+
346
+ // 处理多行内容 - 检测价格+说明的多行格式
347
+ if (text.includes('\n')) {
348
+ const lines = text.trim().split('\n');
349
+
350
+ // 检查第一行是否为价格数字
351
+ if (lines.length >= 2 && /^(\$)?\s*\d+(\.\d+)?\s*$/.test(lines[0].trim())) {
352
+ // 检查第二行是否为价格相关说明
353
+ const secondLine = lines[1].trim();
354
+ if (
355
+ /^\*\*.*?(价格|原价|折扣|优惠|促销|售价).*?\*\*\s*:/.test(secondLine) ||
356
+ /^\*\*(Price|Original|Discount|Sale|Special).*?\*\*\s*:/i.test(secondLine)
357
+ ) {
358
+ return false; // 这是价格+说明的多行格式,不是数学公式
359
+ }
360
+ }
361
+
362
+ // 新增: 检查是否包含Markdown标题行,如果是,整个文本块不是数学公式
363
+ for (const line of lines) {
364
+ if (/^#{1,6}\s+/.test(line.trim())) {
365
+ return false;
366
+ }
367
+ }
368
+ }
369
+
370
+ const trimmedText = text.trim();
371
+
372
+ // 新增: 直接排除包含Markdown格式的解释文本
373
+ if (/^(Based on|According to|From|In|The) .*(answer|correct|material|provided|explanation).*:?$/i.test(trimmedText)) {
374
+ return false;
375
+ }
376
+
377
+ // 新增: 检测解释性文本中的美元金额
378
+ // 匹配形如 "at least $300" 或 "the fine is **$300**" 的模式
379
+ if (/(at least|\bat\b|\bof\b|\bis\b|\bfine\b|\bpay\b|\bfees?\b|\bprice\b|\bcosts?\b|\bpenalty|\bpenalties)\s+(\*\*)?\$\d+/i.test(text)) {
380
+ return false;
381
+ }
382
+
383
+ // 新增: 检测列表项中的美元金额
384
+ if (/^[-•*]\s+.*\$\d+/.test(text) || /^[\d]+\.?\s+.*\$\d+/.test(text)) {
385
+ return false;
386
+ }
387
+
388
+ // 新增: 检测表格引用和解释中的美元金额
389
+ if (/table\s+\d+|general\s+impairment|high\s+rate|highest\s+rate/i.test(text) && /\$\d+/.test(text)) {
390
+ return false;
391
+ }
392
+
393
+ // 1. 检测常见价格模式 (无论是否有美元符号)
394
+ if (/^(\$)?\s*\d+(\.\d+)?\s*$/.test(trimmedText)) {
395
+ return false; // 纯数字或带美元符号的数字,如 "25.99" 或 "$25.99"
396
+ }
397
+
398
+ // 2. 检查商品价格相关上下文
399
+ const priceContextPatterns = [
400
+ // 中文价格相关描述
401
+ /(原价|现价|优惠价|折扣价|促销价|售价|定价|价格|折扣信息|优惠信息)\s*[::]?\s*$/,
402
+ /(价格|费用|金额)\s*[::]?\s*$/,
403
+ /^\s*([¥$€₤₹₽¢₩]|USD|CAD|EUR|GBP|JPY|RMB|CNY)/, // 各种货币符号或货币代码开头
404
+
405
+ // 英文价格相关描述
406
+ /\b(quotation|budget|controller|department|head|manager|director|verbal|approval|vendor|supplier)\b/i,
407
+ /^(price|cost|fee|amount|fine|penalty|minimum)\s*:?\s*$/i,
408
+ /(original|discount|sale|special|retail|list|offer)\s+(price|cost)\s*:?\s*$/i,
409
+ /(price|cost|fine)\s+(before|after|is|of|at least)\s+(tax|discount)\s*:?\s*$/i,
410
+ // 新增: 匹配形如 "minimum fine"、"penalty is" 的模式
411
+ /(minimum|maximum|guaranteed)\s+(fine|penalty|cost|payment)/i
412
+ ];
413
+
414
+ for (const pattern of priceContextPatterns) {
415
+ if (pattern.test(trimmedText)) {
416
+ return false;
417
+ }
418
+ }
419
+
420
+ // 检查常见明显非公式模式
421
+ const notFormulaReg = [
422
+ // 表格特征
423
+ /\|.*\|/, // 包含竖线分隔符
424
+ /\n.*\|/, // 多行表格
425
+
426
+ /^\s*\$?\d{1,3}(,\d{3})+(\.\d{2})?\s*$/, // 千分位货币:$20,000
427
+ /\$\d+,\d+/,
428
+ // 带$的千分位数字
429
+ /[A-Z][a-z]+\s+[A-Z][a-z]+/ //// 首字母大写,英文短语Budget Controllers
430
+ ]
431
+ for (const p of notFormulaReg) {
432
+ if (p.test(trimmedText)) {
433
+ return false;
434
+ }
435
+ }
436
+
437
+ // 3. 检测价格附近的描述文本 - 扩展匹配范围
438
+ const priceDescriptionPatterns = [
439
+ // 商品描述标记
440
+ /^\*\*[\s\S]*?(价格|折扣|优惠|促销|售价|特价|打折|fine|penalty|payment|fee)[\s\S]*?\*\*\s*:?$/i,
441
+ // 商品规格、价格等结构化信息
442
+ /^\*\*(商品|产品|规格|型号|尺寸|数量|Table|BAC|Rate|offense|conviction|Explanation|Notes|Additional)[\s\S]*?\*\*\s*:?$/i,
443
+ ];
444
+
445
+ for (const pattern of priceDescriptionPatterns) {
446
+ if (pattern.test(trimmedText)) {
447
+ return false;
448
+ }
449
+ }
450
+
451
+ // 新增: 检测出现在句中的货币金额 (如 "$300 across all impairment levels")
452
+ if (/\$\d+(\.\d+)?([\s,]+(across|for|in|when|per|each|every|all|any|most|minimum|maximum))/i.test(text)) {
453
+ return false;
454
+ }
455
+
456
+ // 4. 检查是否是特定格式的数字,如只有小数价格
457
+ if (/^(\d+)?\.?\d+$/.test(trimmedText)) {
458
+ return false; // 仅包含数字和小数点的情况,如 "25.99"、".99" 或 "25"
459
+ }
460
+
461
+ // 新增: 检测带引号的金额表述
462
+ if (/['"][^'"]*\$\d+[^'"]*['"]/.test(text)) {
463
+ return false;
464
+ }
465
+
466
+ // 新增:检测引用格式的内容
467
+ if (/^>.*\$\d+/.test(trimmedText)) {
468
+ return false;
469
+ }
470
+
471
+ // 预处理 - 检查是否为美元符号包裹的内容
472
+ const isMathDelimited = text.startsWith('$') && text.endsWith('$') && text.length > 2;
473
+
474
+ // 如果是美元符号包裹的内容,简化处理
475
+ if (isMathDelimited) {
476
+ const content = text.slice(1, -1);
477
+
478
+ // 排除内容是纯数字+小数点的情况(很可能是价格)
479
+ if (/^\s*\d+(\.\d+)?\s*$/.test(content)) {
480
+ return false;
481
+ }
482
+
483
+ // 新增: 排除变量引用格式
484
+ if (/.*\\`.*/.test(content)) {
485
+ return false;
486
+ }
487
+
488
+ // 新增: 排除包含 # 的内容,避免 macro parameter 错误
489
+ if (content.includes('#')) {
490
+ return false;
491
+ }
492
+
493
+ // 单字母或字母组合在数学环境中
494
+ if (/^[a-zA-Z]+$/.test(content.trim())) {
495
+ return true;
496
+ }
497
+ }
498
+
499
+ // 特别处理:独立的单个字母或两个大写字母组合可能是数学符号
500
+
501
+ // 单字母作为独立的数学符号,常见于几何点、变量等
502
+ if (/^[A-Za-z]$/.test(trimmedText)) {
503
+ return true;
504
+ }
505
+
506
+ // 两个大写字母的组合,常见于向量表示如OA、AB等
507
+ if (/^[A-Z]{2}$/.test(trimmedText)) {
508
+ return true;
509
+ }
510
+
511
+ // 处理反斜杠问题 - 将\\text转换为\text进行判断
512
+ const normalizedText = text.replace(/\\\\(?=\w)/g, "\\");
513
+
514
+ // 数学坐标对或区间模式 - 匹配如 (0,c), [a,b] 形式
515
+ const coordinatePattern = /\([^()]+,[^()]+\)|\[[^[\]]+,[^[\]]+\]/;
516
+ if (coordinatePattern.test(normalizedText)) return true;
517
+
518
+ // 新增: 检查是否是代码块或变量引用(包含反引号的模式)
519
+ if (/\\`/.test(normalizedText)) {
520
+ // 先检测是否符合变量引用模式
521
+ if (/\$[a-zA-Z0-9_]+\\`/.test(normalizedText)) {
522
+ return false;
523
+ }
524
+ }
525
+
526
+ // 新增: 排除包含 # 字符的文本,避免 macro parameter 错误
527
+ if (normalizedText.includes('#')) {
528
+ return false;
529
+ }
530
+
531
+ // 检查是否包含LaTeX命令 - 扩展匹配范围,包括比较运算符
532
+ if (/\\([a-zA-Z]+|[=<>])/.test(normalizedText)) return true;
533
+
534
+ // 简单数学公式模式 - 匹配形如 c = 3, c - 3 = 0 的表达式
535
+ const simpleMathPattern = /\b[a-zA-Z]\s*[\+\-\*\/]?\s*\d+\s*[=<>]?\s*\d*\b/;
536
+ if (simpleMathPattern.test(normalizedText)) return true;
537
+
538
+ // 变量与关系运算符的组合 - 匹配形如 x = y, a < b 的表达式
539
+ const variableRelationPattern = /\b[a-zA-Z]\s*[=<>≤≥]\s*[a-zA-Z0-9]\b/;
540
+ if (variableRelationPattern.test(normalizedText)) return true;
541
+
542
+ // LaTeX关系运算符和比较运算符
543
+ const latexOperators = [
544
+ "geq", "leq", "gt", "lt", "neq", "approx", "equiv",
545
+ "geqslant", "leqslant", "sim", "simeq", "cong"
546
+ ];
547
+
548
+ // 检查常见LaTeX关系运算符
549
+ const relationPattern = new RegExp(`\\\\(${latexOperators.join('|')})`);
550
+ if (relationPattern.test(normalizedText)) return true;
551
+
552
+ // 检查是否包含除了文本外的数学结构
553
+ if (/\\text\{[^}]*\}.*?[=+\-*\/\^_\\]/.test(normalizedText)) return true;
554
+
555
+ // 检查常见的数学结构和符号
556
+ const mathPatterns = [
557
+ // 分数结构
558
+ /\\frac/,
559
+ // 各种括号 - 修改以排除 JSON 格式
560
+ /(?!^\s*[\[\{].*?\\\".*?[\]\}]\s*$)[\{\}\(\)\[\]]{2,}/,
561
+ // 数学运算符
562
+ /[+\-*\/=<>^_]/,
563
+ // 特殊数学符号
564
+ /\\(times|div|cdot|pm|mp|sum|int|infty|partial|sqrt|over)/,
565
+ // 矩阵命令
566
+ /\\(begin|end)\{(matrix|pmatrix|bmatrix|vmatrix)\}/,
567
+ // 求和、积分等大型运算符
568
+ /\\(sum|int|prod|lim)/
569
+ ];
570
+
571
+ for (const pattern of mathPatterns) {
572
+ if (pattern.test(normalizedText)) return true;
573
+ }
574
+
575
+ // 修改最后的数学符号检测,排除 HTML 实体编码的 JSON
576
+ if (normalizedText.match(/[\\^_{}]|\\[a-z]+/) &&
577
+ !normalizedText.match(/\\`/) &&
578
+ !normalizedText.includes('\\&quot;')) {
579
+ return true;
580
+ }
581
+ return false;
582
+ }
583
+ /**
584
+ * 提取字符串中被$或$$符号包围的且满足条件的内容
585
+ * 和双美元符号格式
586
+ * @param {string} input - 输入字符串
587
+ * @return {object} 包含转换后字符串和提取内容的对象
588
+ * 举个例子
589
+ *
590
+ *
591
+ 123$$ 233333 $$ 123
592
+ 不是公式---> 123_PLACEHOLDER1_123
593
+ 是公式---> 123$$ 233333 $$ 123
594
+
595
+ 之后就会进行公式处理,处理之后,会替换回来:
596
+ 123_PLACEHOLDER1_123 -> 123$$ 233333 $$ 123
597
+ *
598
+ */
599
+ export function extractDollarEnclosedStrings(input) {
600
+ // console.log('extractDollarEnclosedStrings', input);
601
+ let extracted = [];
602
+ let result = input;
603
+ let placeholderIndex = 0; // 新增:用于生成递增的占位符索引
604
+
605
+ // 步骤1: 先处理双美元符号 $$...$$(暂时保护它们,不进行替换)
606
+ const doubleDelimiters = [];
607
+ const doubleDollarRegex = /\$\$([\s\S]*?)\$\$/g;
608
+ let doubleDollarMatch;
609
+ let doubleDollarPositions = []; // 记录双$的位置,用于后续判断
610
+
611
+ while ((doubleDollarMatch = doubleDollarRegex.exec(input)) !== null) {
612
+ // 保存双$的起始和结束位置
613
+ doubleDollarPositions.push({
614
+ start: doubleDollarMatch.index,
615
+ end: doubleDollarMatch.index + doubleDollarMatch[0].length - 1
616
+ });
617
+ // 保存匹配到的完整内容
618
+ doubleDelimiters.push(doubleDollarMatch[0]);
619
+ }
620
+
621
+ // 使用一个不太可能出现的标记替换双美元符号内容
622
+ for (let i = 0; i < doubleDelimiters.length; i++) {
623
+ const placeholder = `__DOUBLE_DOLLAR_TEMP_${i}__`;
624
+ result = result.replace(doubleDelimiters[i], placeholder);
625
+ }
626
+
627
+ // 步骤2: 处理 \[...\] 语法
628
+ const bracketRegex = /\\\[([\s\S]*?)\\\]/g;
629
+ let bracketMatch;
630
+ let bracketMatches = []; // 存储带有位置信息的匹配项
631
+
632
+ while ((bracketMatch = bracketRegex.exec(result)) !== null) {
633
+ bracketMatches.push({
634
+ full: bracketMatch[0],
635
+ content: bracketMatch[1],
636
+ start: bracketMatch.index,
637
+ end: bracketMatch.index + bracketMatch[0].length
638
+ });
639
+ }
640
+
641
+ // 从后向前处理 \[...\] 匹配项,避免位置偏移
642
+ for (let i = bracketMatches.length - 1; i >= 0; i--) {
643
+ const m = bracketMatches[i];
644
+ // 检查是否是真正的数学公式,与$...$处理方式相同
645
+ if (!isMathJaxFormula(m.content)) {
646
+ const placeholder = `__PLACEHOLDER_${placeholderIndex}__`;
647
+ extracted[placeholderIndex] = m.full; // 修改:直接按索引赋值
648
+ placeholderIndex++; // 索引递增
649
+ result = result.substring(0, m.start) + placeholder + result.substring(m.end);
650
+ }
651
+ }
652
+
653
+ // 步骤3: 处理单美元符号 $...$
654
+ const singleDollarRegex = /\$([^\$]+?)\$/g;
655
+ let singleDollarMatch;
656
+ const tempResult = result; // 临时存储结果,用于后续恢复
657
+ let matches = [];
658
+
659
+ while ((singleDollarMatch = singleDollarRegex.exec(tempResult)) !== null) {
660
+ const fullMatch = singleDollarMatch[0];
661
+ const content = singleDollarMatch[1];
662
+ const start = singleDollarMatch.index;
663
+ const end = start + fullMatch.length;
664
+
665
+ // 确认这是一个独立的单$公式
666
+ let isInDoubleDollar = false;
667
+ for (const pos of doubleDollarPositions) {
668
+ if (start >= pos.start && end <= pos.end) {
669
+ isInDoubleDollar = true;
670
+ break;
671
+ }
672
+ }
673
+
674
+ if (!isInDoubleDollar && content.trim() !== '') {
675
+ matches.push({
676
+ full: fullMatch,
677
+ content: content,
678
+ start: start,
679
+ end: end
680
+ });
681
+ }
682
+ }
683
+
684
+ // 从后向前替换,避免位置偏移
685
+ for (let i = matches.length - 1; i >= 0; i--) {
686
+ const m = matches[i];
687
+ // 检查是否是真正的数学公式
688
+ console.log('isMathJaxFormula111');
689
+ if (!isMathJaxFormula(m.content)) {
690
+ const placeholder = `__PLACEHOLDER_${placeholderIndex}__`;
691
+ extracted[placeholderIndex] = m.full; // 修改:直接按索引赋值
692
+ placeholderIndex++; // 索引递增
693
+ result = result.substring(0, m.start) + placeholder + result.substring(m.end);
694
+ }
695
+ }
696
+
697
+ // 步骤4: 恢复双美元符号内容的临时替换
698
+ for (let i = 0; i < doubleDelimiters.length; i++) {
699
+ const placeholder = `__DOUBLE_DOLLAR_TEMP_${i}__`;
700
+ result = result.replace(placeholder, doubleDelimiters[i]);
701
+ }
702
+
703
+ console.log('extractDollarEnclosedStrings1', result, extracted);
704
+ return {
705
+ transformed: result,
706
+ extracted: extracted
707
+ };
708
+ }
709
+
710
+ /**
711
+ * 将提取的内容还原回原始字符串
712
+ * @param {string} transformed - 转换后的字符串
713
+ * @param {array} extracted - 提取出的内容数组
714
+ * @return {string} 还原后的原始字符串
715
+ */
716
+ export function restoreString(transformed, extracted, doubleDelimiters = []) {
717
+ let result = transformed;
718
+
719
+ // 1. 还原所有占位符,包括单美元符号和 \[...\] 的占位符
720
+ const singleRegex = new RegExp("__PLACEHOLDER_(\\d+)__", "g");
721
+ result = result.replace(singleRegex, (match, index) => {
722
+ const idx = parseInt(index, 10);
723
+ return idx < extracted.length ? extracted[idx] : match;
724
+ });
725
+
726
+ // 2. 再还原双美元符号的占位符
727
+ const doubleRegex = new RegExp("__DOUBLE_DOLLAR_TEMP_(\\d+)__", "g");
728
+ result = result.replace(doubleRegex, (match, index) => {
729
+ const idx = parseInt(index, 10);
730
+ return idx < doubleDelimiters.length ? doubleDelimiters[idx] : match;
731
+ });
732
+
733
+ return result;
734
+ }
735
+ // 自定义白名单
736
+ const myWhiteList = getDefaultWhiteList();
737
+ myWhiteList.a = ['href', 'title', 'target']; // 允许 a 标签的 href, title 和 target 属性
738
+ myWhiteList.source = ['src', 'type']; // 允许 source 标签用于 video/audio
739
+
740
+ console.log('myWhiteList', myWhiteList);
741
+
742
+ export const xssHtmlOptions = {
743
+ css: false,
744
+ whiteList: myWhiteList,
745
+ // {
746
+ // ...,
747
+ // // a: ['class', 'style', 'target', 'data-link-url', 'rel', 'href', 'data-link-url'],
748
+ // // table: ['width', 'border', 'cellspacing', 'cellpadding'],
749
+ // // tr: [],
750
+ // // td: ['width', 'colspan', 'rowspan'],
751
+ // // th: ['width', 'colspan', 'rowspan'],
752
+ // // tbody: [],
753
+ // // thead: [],
754
+ // // tfoot: []
755
+ // // audio: ['class', 'style', 'controls', 'controlsList', 'src'],
756
+ // // video: ['class', 'style', 'controls', 'width', 'height', 'webkit-playsinline', 'playsinline', 'x5-playsinline', 'poster', 'src'],
757
+ // // p: ['class', 'style'],
758
+ // // strong: ['class', 'style'],
759
+ // // span: ['contenteditable'],
760
+ // // div: ['class', 'style', ''],
761
+ // },
762
+ onTag: function (tag, html, options) {
763
+ if (tag.startsWith('mjx-') || tag.startsWith('mjx') || tag.startsWith('mj')) {
764
+ return html;
765
+ }
766
+ return null;
767
+ },
768
+ // 文档地址:https://www.npmjs.com/package/xss
769
+ onTagAttr (tag, name, value, isWhiteAttr) {
770
+ const hasDangerousChars = (val) => {
771
+ if (!val) return false;
772
+ // 检测以下危险模式:
773
+ // 1. 包含 < 或 > 字符(可能是标签注入)
774
+ // 2. 包含引号后紧跟 > 或空格+>(属性闭合攻击)
775
+ // 3. 包含 = 后紧跟引号再跟 >(属性闭合攻击变种)
776
+ // 4. 包含 javascript: 协议
777
+ // 5. 包含事件处理器关键字(onerror, onload 等)
778
+ // 6. 包含 on 开头的事件属性模式(支持 / 或空格作为分隔符)
779
+ return /[<>]|[\"']\\s*>|=\\s*[\"'][^\"']*>|javascript:|data:text\/html|onerror|onload|onclick|onmouseover|on\\w+\\s*=|vbscript:/i.test(val);
780
+ };
781
+
782
+ // 首先检查属性值是否包含危险字符
783
+ if (hasDangerousChars(value)) {
784
+ console.log(`阻止包含危险字符的属性: ${tag} ${name}="${value}"`);
785
+ return ''; // 移除包含危险字符的属性
786
+ }
787
+
788
+ if (tag.indexOf('_') !== -1) {
789
+ // 如果包含下划线,则认为是自定义标签,放过
790
+ return `${name}="${value}"`;
791
+ }
792
+ const tagIgnore = ['div', 'span', 'strong', 'p', 'mark'];
793
+ if (tagIgnore.includes(tag)) {
794
+ // do not filter its attributes
795
+ const blockAttrsReg = new RegExp('^on');
796
+ if (!blockAttrsReg.test(name)) {
797
+ return `${name}="${value}"`;
798
+ }
799
+ }
800
+ const tagAttrIgnore = ['class', 'style'];
801
+ if (tagAttrIgnore.includes(name)) {
802
+ return `${name}="${value}"`;
803
+ }
804
+ // 自定义属性省略
805
+ const aTagAttrIgnore = ['rel', 'data-link-url'];
806
+ if (tag === 'a' && aTagAttrIgnore.includes(name)) {
807
+ return `${name}="${value}"`;
808
+ }
809
+ if (tag === 'a' && name === 'href') {
810
+ // 允许的协议列表
811
+ const allowedProtocols = ['qdweb', 'qdim', 'mqqwpa'];
812
+ const protocol = value.split(':')[0];
813
+ if (allowedProtocols.includes(protocol)) {
814
+ return `${name}="${escapeAttrValue(value)}"`;
815
+ }
816
+ }
817
+ const videoTagAttrIgnore = [
818
+ 'rel',
819
+ 'webkit-playsinline',
820
+ 'x5-playsinline',
821
+ 'title',
822
+ 'size',
823
+ 'format'
824
+ ];
825
+ if (tag === 'video' && videoTagAttrIgnore.includes(name)) {
826
+ return `${name}="${value}"`;
827
+ }
828
+ const audioTagAttrIgnore = [
829
+ 'controls',
830
+ 'controlslist',
831
+ 'id',
832
+ 'speed',
833
+ 'title',
834
+ 'size',
835
+ 'format',
836
+ 'data-duration-ms'
837
+ ];
838
+ if (tag === 'audio' && audioTagAttrIgnore.includes(name)) {
839
+ return `${name}="${value}"`;
840
+ }
841
+ const imgTagAttrIgnore = ['alt', 'data-value'];
842
+ if (tag === 'img' && imgTagAttrIgnore.includes(name)) {
843
+ return `${name}="${value}"`;
844
+ }
845
+ // 允许所有 mjx- 开头的标签的 class 和 style 属性
846
+ if ((tag.startsWith('mjx-') || tag.startsWith('mjx') || tag.startsWith('mj') || tag.startsWith('mo')) && (name === 'class' || name === 'style')) {
847
+ // console.log('tag.startsWith 2', tag, name);
848
+ return `${name}="${escapeAttrValue(value)}"`;
849
+ }
850
+ // if (!isWhiteAttr) {
851
+ // return '';
852
+ // }
853
+ }
854
+ };
855
+
856
+ // 特殊字符映射表
857
+ const SPECIAL_CHAR_MAP = {
858
+ '$@$': 'DEOLLERQ1',
859
+ '$@': 'DEOLLERQ2'
860
+ };
861
+
862
+ // 反向映射表
863
+ const REVERSE_SPECIAL_CHAR_MAP = Object.fromEntries(
864
+ Object.entries(SPECIAL_CHAR_MAP).map(([k, v]) => [v, k])
865
+ );
866
+
867
+ /**
868
+ * 替换特殊字符
869
+ * @param {string} input 输入字符串
870
+ * @returns {string} 替换后的字符串
871
+ */
872
+ export const replaceSpecialChars = (input) => {
873
+ let result = input;
874
+
875
+ // 遍历映射表进行替换
876
+ Object.entries(SPECIAL_CHAR_MAP).forEach(([original, replacement]) => {
877
+ // 使用全局替换
878
+ result = result.split(original).join(replacement);
879
+ });
880
+
881
+ return result;
882
+ }
883
+
884
+ /**
885
+ * 还原特殊字符
886
+ * @param {string} input 输入字符串
887
+ * @returns {string} 还原后的字符串
888
+ */
889
+ export const restoreSpecialChars = (input) => {
890
+ let result = input;
891
+
892
+ // 遍历反向映射表进行还原
893
+ Object.entries(REVERSE_SPECIAL_CHAR_MAP).forEach(([replacement, original]) => {
894
+ // 使用全局替换
895
+ result = result.split(replacement).join(original);
896
+ });
897
+
898
+ return result;
899
+ }
900
+
901
+ // 检测是否支持负向后瞻
902
+ let supportsLookbehind = undefined;
903
+ export const checkIsSupportsLookbehind = () => {
904
+ if(typeof supportsLookbehind === 'boolean') {
905
+ return supportsLookbehind;
906
+ }
907
+ try {
908
+ new RegExp('(?<!x)y');
909
+ supportsLookbehind = true;
910
+ } catch (e) {
911
+ supportsLookbehind = false;
912
+ }
913
+ return supportsLookbehind;
914
+ }
915
+
916
+ export const fixEmptyMarkdownLinks = (markdown) => {
917
+ console.log(9999, checkIsSupportsLookbehind());
918
+ if(checkIsSupportsLookbehind()) {
919
+ // 使用负向后瞻(?<!!)确保我们不匹配图片语法![](url)
920
+ const res = markdown.replace(new RegExp('(?<!!)\\[\\]\\((https?:\\/\\/[^)]+)\\)', 'g'), function(match, url) {
921
+ return '[' + url + '](' + url + ')';
922
+ });
923
+ return res;
924
+ } else {
925
+ // 捕获可能的空链接,包括前面可能的感叹号
926
+ return markdown.replace(new RegExp('(\\!?)(\\[\\]\\((https?:\\/\\/[^)]+)\\))', 'g'), function(match, exclamation, fullMatch, url) {
927
+ // 如果前面有感叹号,说明是图片语法,保持不变
928
+ if (exclamation === '!') {
929
+ return match;
930
+ }
931
+ // 否则进行替换
932
+ return '[' + url + '](' + url + ')';
933
+ });
934
+ }
935
+ }