Files
qiming/qiming-mobile/utils/markdown.uts

198 lines
5.5 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const defaultDelimiters = [
{ left: '\\[', right: '\\]', display: true },
{ left: '\\(', right: '\\)', display: false },
];
// 转义括号规则 - 通用数学公式解析器
function escapedBracketRule(delimiters: any) {
return (text: string, startPos: number = 0) => {
const max = text.length;
const start = startPos;
for (const { left, right, display } of delimiters) {
// 检查是否以左标记开始
if (!text.slice(start).startsWith(left)) continue;
// 跳过左标记的长度
let pos = start + left.length;
// 寻找匹配的右标记
while (pos < max) {
if (text.slice(pos).startsWith(right)) {
break;
}
pos++;
}
// 没找到匹配的右标记,跳过,进入下个匹配
if (pos >= max) continue;
// 提取数学公式内容
const content = text.slice(start + left.length, pos);
const endPos = pos + right.length;
return {
formula: content,
display,
start,
end: endPos,
left,
right,
success: true,
};
}
return {
formula: '',
display: false,
start: 0,
end: 0,
left: '',
right: '',
success: false,
};
};
}
/**
* 根据规则将连续的过程标签ToolCall, Skill等合并
* 规则:
* 1. 连续 2 个及以上的过程标签合并
* 2. 中间包含“执行计划”的不合并(作为分隔符)
* 3. 标签间只包含空白字符时不中断合并
* 4. 支持 :::container 语法和 PC 端的 HTML 标签语法
* @param text - 待处理的 Markdown 文本
* @returns 处理后的文本
*/
export function groupMarkdownContainers(text : string) : string {
if (!text) return '';
// 1. 匹配 HTML 格式的过程标签 (markdown-custom-process)
// 2. 匹配 :::container 格式的标签
const blockRegex = /(?:[\r\n\s]*)(?:(?:<(?:div|p)\b[^>]*>)?\s*(<markdown-custom-process\b[^>]*?>(?:<\/markdown-custom-process>|\/>)?)\s*(?:<\/(?:div|p)>)?|(:::container\b[^\n]*?\n:::))/g;
let result = '';
let lastIndex = 0;
let currentGroup : string[] = [];
let match : RegExpExecArray | null;
const flushGroup = () => {
if (currentGroup.length > 0) {
if (currentGroup.length >= 2) {
// 多个时,为了支持层级,必须使用规范化后的 HTML 标签
const groupContent = `\n\n<markdown-custom-process-group>\n${normalizedGroup.join('\n')}\n</markdown-custom-process-group>\n\n`;
result += groupContent;
} else {
// 只有一个工具时,保持原样输出,最稳妥
result += `\n\n${currentGroup[0]}\n\n`;
}
currentGroup = [];
normalizedGroup = [];
}
};
let count = 0;
let normalizedGroup : string[] = [];
while ((match = blockRegex.exec(text)) !== null) {
count++;
const textBefore = text.slice(lastIndex, match.index);
if (textBefore.trim() !== '') {
flushGroup();
result += textBefore;
} else {
if (currentGroup.length === 0) {
result += textBefore;
}
}
let tagMatch = match[1] || match[2] || '';
// 为了分组显示,我们依然需要一份规范化后的 HTML 标签
let normalizedTag = '';
if (tagMatch.startsWith(':::container')) {
const executeId = extractAttribute(tagMatch, 'executeId');
const type = extractAttribute(tagMatch, 'type');
const status = extractAttribute(tagMatch, 'status');
const name = extractAttribute(tagMatch, 'name');
const safeName = name.replace(/"/g, '&quot;');
normalizedTag = `<markdown-custom-process executeid="${executeId}" type="${type || ''}" status="${status || ''}" name="${safeName}"></markdown-custom-process>`;
} else {
normalizedTag = tagMatch;
if (!normalizedTag.includes('</markdown-custom-process>') && !normalizedTag.endsWith('/>')) {
normalizedTag = normalizedTag.replace('>', '></markdown-custom-process>');
}
}
const isPlan = /type=["']Plan["']/.test(normalizedTag);
if (isPlan) {
flushGroup();
// Plan 类型保持原样输出
result += `\n\n${tagMatch}\n\n`;
} else {
currentGroup.push(tagMatch);
normalizedGroup.push(normalizedTag);
}
lastIndex = blockRegex.lastIndex;
}
flushGroup();
result += text.slice(lastIndex);
const finalResult = result.replace(/\n{3,}/g, '\n\n').trim();
return finalResult;
}
/**
* 从属性字符串中提取指定属性的值
*/
function extractAttribute(attrString : string, attrName : string) : string {
const regex = new RegExp(`${attrName}=["']([^"']*)["']`);
const match = attrString.match(regex);
if (match != null) {
const val = match[1];
// 还原 HTML 实体,避免在后续处理中出现双重转义
return val.replace(/&quot;/g, '"');
}
return '';
}
// 新的数学公式替换函数 - 直接替换为 $$ 分隔符
export function replaceMathBracket(text : string) : string {
// return text; //临时关闭
// 创建只包含非美元符号分隔符的选项
const nonDollarDelimiters = defaultDelimiters.filter(
(delimiter) =>
!delimiter.left.includes('$') && !delimiter.right.includes('$'),
);
const rule = escapedBracketRule(nonDollarDelimiters);
let result = '';
let pos = 0;
while (pos < text.length) {
const match = rule(text, pos);
if (match.success) {
// 添加匹配前的文本
result += text.slice(pos, match.start);
// 替换为 $$ 分隔符
const delimiter = match.display ? '$$' : '$';
result += `${delimiter}${match.formula}${delimiter}`;
pos = match.end;
} else {
// 没有匹配,添加当前字符
result += text[pos];
pos++;
}
}
return result;
}