@will1123/lx-ui-utils 1.0.1 → 1.0.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","sources":["../../src/markdown.ts"],"sourcesContent":["/**\r\n * Markdown 渲染配置\r\n */\r\nexport interface MarkdownRenderConfig {\r\n /** 是否启用代码高亮(默认 false) */\r\n highlight?: boolean\r\n /** 自定义代码高亮语言映射 */\r\n highlightLanguages?: Record<string, string>\r\n /** 是否渲染任务列表(默认 true) */\r\n taskList?: boolean\r\n /** 是否渲染表格(默认 true) */\r\n table?: boolean\r\n /** 是否渲染链接(默认 true) */\r\n link?: boolean\r\n /** 是否渲染图片(默认 true) */\r\n image?: boolean\r\n}\r\n\r\n/**\r\n * Markdown 流式更新配置\r\n */\r\nexport interface MarkdownStreamConfig extends MarkdownRenderConfig {\r\n /** 更新回调函数 */\r\n onUpdate?: (html: string) => void\r\n /** 完成回调函数 */\r\n onComplete?: (html: string) => void\r\n}\r\n\r\n/**\r\n * 简单的 Markdown 渲染器\r\n */\r\nexport class MarkdownRenderer {\r\n private config: MarkdownRenderConfig\r\n\r\n constructor(config: MarkdownRenderConfig = {}) {\r\n this.config = {\r\n highlight: config.highlight || false,\r\n highlightLanguages: config.highlightLanguages || {},\r\n taskList: config.taskList !== false,\r\n table: config.table !== false,\r\n link: config.link !== false,\r\n image: config.image !== false\r\n }\r\n }\r\n\r\n /**\r\n * 渲染 Markdown 为 HTML\r\n */\r\n render(markdown: string): string {\r\n let html = markdown\r\n\r\n // 转义 HTML 特殊字符(除了 Markdown 语法)\r\n html = this.escapeHtml(html)\r\n\r\n // 代码块(需要在其他处理之前)\r\n html = this.renderCodeBlocks(html)\r\n\r\n // 标题\r\n html = this.renderHeadings(html)\r\n\r\n // 粗体和斜体\r\n html = this.renderEmphasis(html)\r\n\r\n // 任务列表\r\n if (this.config.taskList) {\r\n html = this.renderTaskLists(html)\r\n }\r\n\r\n // 无序列表\r\n html = this.renderUnorderedLists(html)\r\n\r\n // 有序列表\r\n html = this.renderOrderedLists(html)\r\n\r\n // 表格\r\n if (this.config.table) {\r\n html = this.renderTables(html)\r\n }\r\n\r\n // 链接\r\n if (this.config.link) {\r\n html = this.renderLinks(html)\r\n }\r\n\r\n // 图片\r\n if (this.config.image) {\r\n html = this.renderImages(html)\r\n }\r\n\r\n // 段落\r\n html = this.renderParagraphs(html)\r\n\r\n // 换行\r\n html = this.renderLineBreaks(html)\r\n\r\n return html\r\n }\r\n\r\n /**\r\n * 转义 HTML 特殊字符\r\n */\r\n private escapeHtml(text: string): string {\r\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\r\n }\r\n\r\n /**\r\n * 渲染代码块\r\n */\r\n private renderCodeBlocks(text: string): string {\r\n // 代码块(支持语法高亮标记)\r\n return text.replace(\r\n /`{3}(\\w*)\\n([\\s\\S]*?)`{3}/g,\r\n (match, lang, code) => {\r\n const language = lang || 'text'\r\n return `<pre><code class=\"language-${language}\">${code.trim()}</code></pre>`\r\n }\r\n )\r\n }\r\n\r\n /**\r\n * 渲染标题\r\n */\r\n private renderHeadings(text: string): string {\r\n return text\r\n .replace(/^#{6}\\s+(.+)$/gm, '<h6>$1</h6>')\r\n .replace(/^#{5}\\s+(.+)$/gm, '<h5>$1</h5>')\r\n .replace(/^#{4}\\s+(.+)$/gm, '<h4>$1</h4>')\r\n .replace(/^#{3}\\s+(.+)$/gm, '<h3>$1</h3>')\r\n .replace(/^#{2}\\s+(.+)$/gm, '<h2>$1</h2>')\r\n .replace(/^#{1}\\s+(.+)$/gm, '<h1>$1</h1>')\r\n }\r\n\r\n /**\r\n * 渲染粗体和斜体\r\n */\r\n private renderEmphasis(text: string): string {\r\n // 粗体\r\n text = text.replace(/\\*\\*\\*(.+?)\\*\\*\\*/g, '<strong><em>$1</em></strong>')\r\n text = text.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\r\n text = text.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>')\r\n text = text.replace(/__(.+?)__/g, '<strong>$1</strong>')\r\n\r\n // 斜体\r\n text = text.replace(/\\*(.+?)\\*/g, '<em>$1</em>')\r\n text = text.replace(/_(.+?)_/g, '<em>$1</em>')\r\n\r\n return text\r\n }\r\n\r\n /**\r\n * 渲染任务列表\r\n */\r\n private renderTaskLists(text: string): string {\r\n return text.replace(/^\\s*-\\s*\\[x\\]\\s+(.+)$/gm, '<li class=\"task-checked\"><input type=\"checkbox\" checked disabled> $1</li>')\r\n .replace(/^\\s*-\\s*\\[\\s*\\]\\s+(.+)$/gm, '<li><input type=\"checkbox\" disabled> $1</li>')\r\n }\r\n\r\n /**\r\n * 渲染无序列表\r\n */\r\n private renderUnorderedLists(text: string): string {\r\n const lines = text.split('\\n')\r\n const result: string[] = []\r\n const listStack: string[] = []\r\n\r\n for (const line of lines) {\r\n const match = line.match(/^(\\s*)-\\s+(.+)$/)\r\n if (!match) {\r\n // 关闭所有打开的列表\r\n while (listStack.length > 0) {\r\n result.push(listStack.pop() || '')\r\n }\r\n result.push(line)\r\n continue\r\n }\r\n\r\n const [_, indent, content] = match\r\n const indentLevel = indent.length\r\n const listTag = '<ul>'\r\n\r\n // 处理缩进层级\r\n while (listStack.length > 0 && listStack.length < indentLevel / 2 + 1) {\r\n result.push(listStack.pop() || '')\r\n }\r\n\r\n // 打开新的列表\r\n while (listStack.length < indentLevel / 2 + 1) {\r\n listStack.push('<ul>')\r\n result.push('<ul>')\r\n }\r\n\r\n result.push(`<li>${content}</li>`)\r\n }\r\n\r\n // 关闭所有打开的列表\r\n while (listStack.length > 0) {\r\n result.push('</ul>')\r\n listStack.pop()\r\n }\r\n\r\n return result.join('\\n')\r\n }\r\n\r\n /**\r\n * 渲染有序列表\r\n */\r\n private renderOrderedLists(text: string): string {\r\n const lines = text.split('\\n')\r\n const result: string[] = []\r\n let inList = false\r\n\r\n for (const line of lines) {\r\n const match = line.match(/^\\s*\\d+\\.\\s+(.+)$/)\r\n if (!match) {\r\n if (inList) {\r\n result.push('</ol>')\r\n inList = false\r\n }\r\n result.push(line)\r\n continue\r\n }\r\n\r\n const [_, content] = match\r\n if (!inList) {\r\n result.push('<ol>')\r\n inList = true\r\n }\r\n result.push(`<li>${content}</li>`)\r\n }\r\n\r\n if (inList) {\r\n result.push('</ol>')\r\n }\r\n\r\n return result.join('\\n')\r\n }\r\n\r\n /**\r\n * 渲染表格\r\n */\r\n private renderTables(text: string): string {\r\n const lines = text.split('\\n')\r\n const result: string[] = []\r\n let inTable = false\r\n let headerRendered = false\r\n\r\n for (let i = 0; i < lines.length; i++) {\r\n const line = lines[i]\r\n\r\n // 表格分隔符\r\n if (line.match(/^\\|[\\s\\-:|]+\\|$/)) {\r\n continue\r\n }\r\n\r\n // 表格行\r\n const match = line.match(/^\\|(.+)\\|$/)\r\n if (!match) {\r\n if (inTable) {\r\n result.push('</table>')\r\n inTable = false\r\n headerRendered = false\r\n }\r\n result.push(line)\r\n continue\r\n }\r\n\r\n const cells = match[1].split('|').map((cell: string) => cell.trim())\r\n\r\n if (!inTable) {\r\n result.push('<table>')\r\n inTable = true\r\n }\r\n\r\n if (!headerRendered) {\r\n result.push('<thead><tr>')\r\n cells.forEach((cell: string) => {\r\n result.push(`<th>${cell}</th>`)\r\n })\r\n result.push('</tr></thead><tbody>')\r\n headerRendered = true\r\n } else {\r\n result.push('<tr>')\r\n cells.forEach((cell: string) => {\r\n result.push(`<td>${cell}</td>`)\r\n })\r\n result.push('</tr>')\r\n }\r\n }\r\n\r\n if (inTable) {\r\n result.push('</tbody></table>')\r\n }\r\n\r\n return result.join('\\n')\r\n }\r\n\r\n /**\r\n * 渲染链接\r\n */\r\n private renderLinks(text: string): string {\r\n return text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" target=\"_blank\">$1</a>')\r\n }\r\n\r\n /**\r\n * 渲染图片\r\n */\r\n private renderImages(text: string): string {\r\n return text.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" />')\r\n }\r\n\r\n /**\r\n * 渲染段落\r\n */\r\n private renderParagraphs(text: string): string {\r\n const lines = text.split('\\n')\r\n const result: string[] = []\r\n let inParagraph = false\r\n\r\n for (const line of lines) {\r\n // 跳过空行和 HTML 标签\r\n if (!line.trim() || line.match(/^<(h|ul|ol|li|table|pre|div)/)) {\r\n if (inParagraph) {\r\n result.push('</p>')\r\n inParagraph = false\r\n }\r\n result.push(line)\r\n continue\r\n }\r\n\r\n if (!inParagraph) {\r\n result.push('<p>')\r\n inParagraph = true\r\n }\r\n result.push(line)\r\n }\r\n\r\n if (inParagraph) {\r\n result.push('</p>')\r\n }\r\n\r\n return result.join('\\n')\r\n }\r\n\r\n /**\r\n * 渲染换行\r\n */\r\n private renderLineBreaks(text: string): string {\r\n // 在段落内的换行转换为 <br>\r\n return text.replace(/(<p>.*?<\\/p>|<li>.*?<\\/li>)/g, (match) => {\r\n return match.replace(/\\n/g, '<br>\\n')\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 渲染 Markdown 为 HTML(便捷方法)\r\n */\r\nexport function renderMarkdown(\r\n markdown: string,\r\n config?: MarkdownRenderConfig\r\n): string {\r\n const renderer = new MarkdownRenderer(config)\r\n return renderer.render(markdown)\r\n}\r\n\r\n/**\r\n * 流式渲染 Markdown(用于 SSE 流式响应)\r\n */\r\nexport class MarkdownStreamRenderer {\r\n private renderer: MarkdownRenderer\r\n private buffer: string = ''\r\n private config: MarkdownStreamConfig\r\n\r\n constructor(config: MarkdownStreamConfig = {}) {\r\n this.config = config\r\n this.renderer = new MarkdownRenderer(config)\r\n }\r\n\r\n /**\r\n * 追加文本并渲染\r\n */\r\n append(text: string): string {\r\n this.buffer += text\r\n const html = this.renderer.render(this.buffer)\r\n\r\n if (this.config.onUpdate) {\r\n this.config.onUpdate(html)\r\n }\r\n\r\n return html\r\n }\r\n\r\n /**\r\n * 完成渲染\r\n */\r\n complete(): string {\r\n const html = this.renderer.render(this.buffer)\r\n\r\n if (this.config.onComplete) {\r\n this.config.onComplete(html)\r\n }\r\n\r\n return html\r\n }\r\n\r\n /**\r\n * 重置渲染器\r\n */\r\n reset(): void {\r\n this.buffer = ''\r\n }\r\n}\r\n\r\n/**\r\n * 创建流式渲染器实例\r\n */\r\nexport function createMarkdownRenderer(\r\n config?: MarkdownStreamConfig\r\n): MarkdownStreamRenderer {\r\n return new MarkdownStreamRenderer(config)\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"markdown.js","sources":["../../src/markdown.ts"],"sourcesContent":["import { marked, Marked } from 'marked';\r\nimport { markedHighlight } from 'marked-highlight';\r\nimport hljs from 'highlight.js';\r\nimport katex from 'katex';\r\nimport { copyToClipboard, isDom } from './helper';\r\n\r\n/**\r\n * 代码主题类型\r\n */\r\nexport type CodeTheme = 'dark' | 'light';\r\n\r\n/**\r\n * createMarkdownRender 函数配置\r\n */\r\nexport interface CreateMarkdownRenderConfig {\r\n /** 代码块工具栏是否启用(默认 true) */\r\n codeBoxToolEnable?: boolean;\r\n /** 是否启用 KaTeX 公式支持(默认 true) */\r\n katexEnable?: boolean;\r\n /** KaTeX 配置选项 */\r\n katexOptions?: katex.KatexOptions;\r\n}\r\n\r\n/**\r\n * 代码块工具栏事件配置\r\n */\r\nexport interface CodeBoxEventsConfig {\r\n /** 复制成功回调 */\r\n onCopySuccess?: (text: string) => void;\r\n /** 复制失败回调 */\r\n onCopyError?: (error: Error) => void;\r\n /** 主题切换回调 */\r\n onThemeChange?: (theme: CodeTheme) => void;\r\n}\r\n\r\n/**\r\n * 从 localStorage 获取代码主题\r\n */\r\nexport function getMarkdownCodeTheme(): CodeTheme {\r\n if (typeof window === 'undefined') return 'dark';\r\n return (localStorage.getItem('LxUIMdCodeTheme') as CodeTheme) || 'dark';\r\n}\r\n\r\n/**\r\n * 设置代码主题到 localStorage\r\n */\r\nexport function setMarkdownCodeTheme(theme: CodeTheme): void {\r\n if (typeof window === 'undefined') return;\r\n localStorage.setItem('LxUIMdCodeTheme', theme);\r\n}\r\n\r\n/**\r\n * 切换代码主题\r\n *\r\n * @returns 新主题\r\n *\r\n * @example\r\n * ```typescript\r\n * import { toggleMarkdownCodeTheme } from '@will1123/lx-ui-utils'\r\n *\r\n * // 切换主题\r\n * const newTheme = toggleMarkdownCodeTheme()\r\n * // 注意:此函数只切换 localStorage 中的主题,不会更新 DOM\r\n * // 如需更新 DOM,请配合 updateMarkdownCodeBlocksTheme\r\n * ```\r\n */\r\nexport function toggleMarkdownCodeTheme(): CodeTheme {\r\n const currentTheme = getMarkdownCodeTheme();\r\n const newTheme = currentTheme === 'dark' ? 'light' : 'dark';\r\n setMarkdownCodeTheme(newTheme);\r\n return newTheme;\r\n}\r\n\r\n/**\r\n * 更新 DOM 中所有代码块的主题\r\n *\r\n * @param themeOrContainer - 主题或容器元素(字符串表示主题,DOM 元素表示容器)\r\n * @param theme - 主题(当第一个参数是容器时使用)\r\n *\r\n * @example\r\n * ```typescript\r\n * import { updateMarkdownCodeBlocksTheme } from '@will1123/lx-ui-utils'\r\n *\r\n * // 方式 1:只传主题(更新 document.body 中的所有代码块)\r\n * updateMarkdownCodeBlocksTheme('dark')\r\n *\r\n * // 方式 2:传容器和主题\r\n * const container = document.querySelector('#markdown-container')\r\n * updateMarkdownCodeBlocksTheme(container, 'light')\r\n * ```\r\n */\r\nexport function updateMarkdownCodeBlocksTheme(\r\n themeOrContainer: CodeTheme | HTMLElement | Document,\r\n theme?: CodeTheme\r\n): void {\r\n if (typeof window === 'undefined') return;\r\n\r\n // 解析参数\r\n let actualTheme: CodeTheme;\r\n let container: HTMLElement | Document = document.body;\r\n\r\n if (typeof themeOrContainer === 'string') {\r\n // 第一个参数是 theme\r\n actualTheme = themeOrContainer;\r\n } else {\r\n // 第一个参数是 container\r\n container = themeOrContainer;\r\n actualTheme = theme as CodeTheme;\r\n }\r\n\r\n const codeBoxes = container.querySelectorAll('.lx-ui-code-box');\r\n codeBoxes.forEach(box => {\r\n (box as HTMLElement).setAttribute('data-theme', actualTheme);\r\n });\r\n}\r\n\r\n/**\r\n * 创建配置好的 Marked 实例\r\n */\r\nexport function createMarkdownRender(\r\n createConfig: CreateMarkdownRenderConfig = {}\r\n) {\r\n const {\r\n codeBoxToolEnable = true,\r\n katexEnable = true,\r\n katexOptions\r\n } = createConfig;\r\n\r\n const markedRenderer = new marked.Renderer();\r\n\r\n // 重写链接渲染器 - 自动添加 target=\"_blank\"\r\n const originalLinkRenderer = markedRenderer.link;\r\n markedRenderer.link = (...arg) => {\r\n const html = originalLinkRenderer.call(markedRenderer, ...arg);\r\n return html.replace(/^<a /, '<a target=\"_blank\" ');\r\n };\r\n\r\n // 重写表格渲染器 - 包装在可滚动容器中\r\n const originalTableRenderer = markedRenderer.table;\r\n markedRenderer.table = (...arg) => {\r\n const html = originalTableRenderer.call(markedRenderer, ...arg);\r\n return `<div class=\"lx-ui-table-wrapper\"><div class=\"lx-ui-table-box\">${html}</div></div>`;\r\n };\r\n\r\n // 重写代码块渲染器 - 添加自定义样式和主题\r\n const originalCodeRenderer = markedRenderer.code;\r\n markedRenderer.code = token => {\r\n const html = originalCodeRenderer.call(markedRenderer, token);\r\n const theme = getMarkdownCodeTheme();\r\n const lang = token?.lang || '';\r\n\r\n // 代码块工具栏 HTML(启用工具栏)\r\n const codeBoxToolEnableHtml = `<div class=\"lx-ui-code-box-header\">\r\n <div class=\"lx-ui-code-box-header-left\">\r\n <span class=\"lx-ui-code-language\">${lang}</span>\r\n </div>\r\n <div class=\"lx-ui-code-box-header-right\">\r\n <i class=\"lx-ui-code-box-header-copy lx-ui-icon-copy\" title=\"复制\">复制</i>\r\n <i class=\"lx-ui-code-box-header-theme lx-ui-icon-theme\" title=\"切换主题\">切换主题</i>\r\n </div>\r\n </div>`;\r\n\r\n // 代码块工具栏 HTML(禁用工具栏)\r\n const codeBoxToolDisableHtml = `<div class=\"lx-ui-code-box-header\">\r\n <div class=\"lx-ui-code-box-header-left\">\r\n <span class=\"lx-ui-code-language\">${lang}</span>\r\n </div>\r\n </div>`;\r\n\r\n return `<div class=\"lx-ui-code-box-wrapper\">\r\n <div class=\"lx-ui-code-box\" data-theme=\"${theme}\">\r\n ${codeBoxToolEnable ? codeBoxToolEnableHtml : codeBoxToolDisableHtml}\r\n <div class=\"lx-ui-code-box-content\">\r\n ${html}\r\n </div>\r\n </div>\r\n </div>`;\r\n };\r\n\r\n // 创建 Marked 实例并配置语法高亮\r\n const markdownRender = new Marked(\r\n markedHighlight({\r\n langPrefix: 'hljs language-',\r\n highlight(code, lang) {\r\n const language = hljs.getLanguage(lang) ? lang : 'plaintext';\r\n return hljs.highlight(code, { language }).value;\r\n }\r\n })\r\n );\r\n\r\n // 设置渲染器选项\r\n markdownRender.setOptions({\r\n renderer: markedRenderer\r\n });\r\n\r\n // 如果启用了 KaTeX,注册 KaTeX 扩展\r\n if (katexEnable) {\r\n markdownRender.use(createKatexExtension(katexOptions));\r\n }\r\n\r\n return markdownRender;\r\n}\r\n\r\n/**\r\n * 创建 KaTeX Marked 扩展\r\n * @param katexOptions - KaTeX 配置选项\r\n * @returns Marked 扩展对象\r\n */\r\nfunction createKatexExtension(katexOptions?: katex.KatexOptions) {\r\n return {\r\n extensions: [\r\n {\r\n name: 'blockMath',\r\n level: 'block' as const,\r\n start(src: string) {\r\n return src.indexOf('$$');\r\n },\r\n tokenizer(src: string) {\r\n const match = src.match(/^\\$\\$([\\s\\S]*?)\\$\\$(?:\\n|$)/);\r\n if (match) {\r\n try {\r\n const formula = match[1].trim();\r\n const html = katex.renderToString(formula, {\r\n displayMode: true,\r\n throwOnError: false,\r\n ...katexOptions\r\n });\r\n return {\r\n type: 'blockMath',\r\n raw: match[0],\r\n html: `<div class=\"lx-ui-katex-block\">${html}</div>`\r\n };\r\n } catch (e) {\r\n console.error('块级公式渲染错误:', e);\r\n return {\r\n type: 'blockMath',\r\n raw: match[0],\r\n html: match[0]\r\n };\r\n }\r\n }\r\n },\r\n renderer(token: any) {\r\n return token.html || '';\r\n }\r\n },\r\n {\r\n name: 'inlineMath',\r\n level: 'inline' as const,\r\n start(src: string) {\r\n return src.indexOf('$');\r\n },\r\n tokenizer(src: string) {\r\n // 匹配 $...$ 但排除 $$...$$\r\n const match = src.match(/^\\$(?!\\$)([^\\$\\n]+?)\\$(?!\\$)/);\r\n if (match) {\r\n try {\r\n const formula = match[1].trim();\r\n const html = katex.renderToString(formula, {\r\n displayMode: false,\r\n throwOnError: false,\r\n ...katexOptions\r\n });\r\n return {\r\n type: 'inlineMath',\r\n raw: match[0],\r\n html: `<span class=\"lx-ui-katex-inline\">${html}</span>`\r\n };\r\n } catch (e) {\r\n console.error('行内公式渲染错误:', e);\r\n return {\r\n type: 'inlineMath',\r\n raw: match[0],\r\n html: match[0]\r\n };\r\n }\r\n }\r\n },\r\n renderer(token: any) {\r\n return token.html || '';\r\n }\r\n }\r\n ]\r\n };\r\n}\r\n\r\n/**\r\n * 绑定代码块工具栏事件(函数重载)\r\n */\r\nexport function bindMarkdownCodeBoxEvents(): () => void;\r\nexport function bindMarkdownCodeBoxEvents(\r\n config: CodeBoxEventsConfig\r\n): () => void;\r\nexport function bindMarkdownCodeBoxEvents(\r\n container: HTMLElement | Document\r\n): () => void;\r\nexport function bindMarkdownCodeBoxEvents(\r\n container: HTMLElement | Document,\r\n config: CodeBoxEventsConfig\r\n): () => void;\r\n\r\n/**\r\n * 绑定代码块工具栏事件\r\n * 使用事件委托处理所有代码块的交互\r\n *\r\n * 参数判断逻辑:\r\n * 1. 先判断是否为 DOM 元素(使用 isDom 函数)\r\n * 2. 再判断是否为 config 对象\r\n *\r\n * @param containerOrConfig - 容器元素或配置对象\r\n * @param config - 事件回调配置(当第一个参数是容器时使用)\r\n * @returns 清理函数,调用可解绑事件\r\n *\r\n * @example\r\n * ```typescript\r\n * import { bindMarkdownCodeBoxEvents } from '@will1123/lx-ui-utils'\r\n *\r\n * // 1. 不传参数(默认 document.body)\r\n * const unbind1 = bindMarkdownCodeBoxEvents()\r\n *\r\n * // 2. 只传 config(默认 document.body)\r\n * const unbind2 = bindMarkdownCodeBoxEvents({\r\n * onCopySuccess: (text) => console.log('已复制:', text)\r\n * })\r\n *\r\n * // 3. 只传 container(使用默认配置)\r\n * const container = document.querySelector('#container')\r\n * const unbind3 = bindMarkdownCodeBoxEvents(container)\r\n *\r\n * // 4. 传 container 和 config\r\n * const unbind4 = bindMarkdownCodeBoxEvents(document.body, {\r\n * onCopySuccess: (text) => console.log('已复制:', text),\r\n * onThemeChange: (theme) => console.log('主题切换为:', theme)\r\n * })\r\n *\r\n * // 组件销毁时解绑\r\n * onUnmounted(() => unbind())\r\n * ```\r\n */\r\nexport function bindMarkdownCodeBoxEvents(\r\n containerOrConfig?: HTMLElement | Document | CodeBoxEventsConfig,\r\n config?: CodeBoxEventsConfig\r\n): () => void {\r\n if (typeof window === 'undefined') {\r\n return () => {};\r\n }\r\n\r\n // 解析参数\r\n let container: HTMLElement | Document = document.body;\r\n let eventsConfig: CodeBoxEventsConfig = {};\r\n\r\n // 判断第一个参数的类型:先判断 DOM,再判断 config\r\n if (containerOrConfig) {\r\n if (isDom(containerOrConfig)) {\r\n // 第一个参数是 container\r\n container = containerOrConfig;\r\n // 第二个参数可能是 config\r\n if (config) {\r\n eventsConfig = config;\r\n }\r\n } else {\r\n // 第一个参数是 config\r\n eventsConfig = containerOrConfig as CodeBoxEventsConfig;\r\n }\r\n }\r\n\r\n const { onCopySuccess, onCopyError, onThemeChange } = eventsConfig;\r\n\r\n /**\r\n * 点击事件处理函数\r\n */\r\n const clickHandler = async (e: Event) => {\r\n const target = e.target as HTMLElement;\r\n\r\n // 处理复制按钮\r\n const copyButton = target.closest('.lx-ui-icon-copy') as HTMLElement;\r\n if (copyButton) {\r\n const codeBox = copyButton.closest('.lx-ui-code-box') as HTMLElement;\r\n const codeElement = codeBox?.querySelector('.lx-ui-code-box-content');\r\n\r\n if (codeElement) {\r\n const text = codeElement.textContent || '';\r\n const success = await copyToClipboard(text);\r\n\r\n if (success) {\r\n onCopySuccess?.(text);\r\n } else {\r\n onCopyError?.(new Error('复制失败'));\r\n }\r\n }\r\n\r\n e.stopPropagation();\r\n e.preventDefault();\r\n return;\r\n }\r\n\r\n // 处理主题切换按钮\r\n const themeButton = target.closest('.lx-ui-icon-theme') as HTMLElement;\r\n if (themeButton) {\r\n // 1. 切换 localStorage 中的主题\r\n const newTheme = toggleMarkdownCodeTheme();\r\n\r\n // 2. 更新指定容器内所有代码块的 data-theme\r\n updateMarkdownCodeBlocksTheme(newTheme);\r\n\r\n // 3. 触发回调\r\n onThemeChange?.(newTheme);\r\n\r\n e.stopPropagation();\r\n e.preventDefault();\r\n return;\r\n }\r\n };\r\n\r\n // 绑定事件\r\n container.addEventListener('click', clickHandler);\r\n\r\n // 返回清理函数\r\n return () => {\r\n container.removeEventListener('click', clickHandler);\r\n };\r\n}\r\n"],"names":[],"mappings":"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"}
package/dist/es/sse.js CHANGED
@@ -34,153 +34,45 @@ var __async = (__this, __arguments, generator) => {
34
34
  step((generator = generator.apply(__this, __arguments)).next());
35
35
  });
36
36
  };
37
- class SSEConnection {
38
- constructor() {
39
- this.controller = null;
40
- this.eventSource = null;
41
- this.timeoutTimer = null;
42
- }
43
- /**
44
- * 发起 SSE 请求
45
- */
46
- request(config, handlers) {
47
- return __async(this, null, function* () {
48
- var _a, _b, _c, _d, _e;
49
- const { url, method = "GET", headers = {}, body, timeout = 3e4 } = config;
50
- this.controller = new AbortController();
51
- try {
52
- if (timeout > 0) {
53
- this.timeoutTimer = setTimeout(() => {
54
- var _a2;
55
- this.close();
56
- (_a2 = handlers.onError) == null ? void 0 : _a2.call(handlers, new Error(`Request timeout after ${timeout}ms`));
57
- }, timeout);
58
- }
59
- const response = yield fetch(url, {
60
- method,
61
- headers: __spreadValues({
62
- "Content-Type": "application/json"
63
- }, headers),
64
- body: method === "POST" ? JSON.stringify(body) : void 0,
65
- signal: this.controller.signal
37
+ import { fetchEventSource } from "@microsoft/fetch-event-source";
38
+ function sse(config, handlers) {
39
+ return __async(this, null, function* () {
40
+ const { url, method = "POST", headers = {}, body, signal, token, openWhenHidden = true } = config;
41
+ const requestHeaders = __spreadValues({
42
+ "Content-Type": "application/json;charset=utf-8"
43
+ }, headers);
44
+ if (token) {
45
+ requestHeaders["Authorization"] = token;
46
+ }
47
+ yield fetchEventSource(url, {
48
+ method,
49
+ headers: requestHeaders,
50
+ body: JSON.stringify(body),
51
+ signal,
52
+ openWhenHidden,
53
+ onopen(response) {
54
+ return __async(this, null, function* () {
55
+ var _a2;
56
+ yield (_a2 = handlers.onOpen) == null ? void 0 : _a2.call(handlers, response);
66
57
  });
67
- if (this.timeoutTimer) {
68
- clearTimeout(this.timeoutTimer);
69
- this.timeoutTimer = null;
70
- }
71
- if (!response.ok) {
72
- throw new Error(`HTTP error! status: ${response.status}`);
73
- }
74
- const contentType = response.headers.get("content-type");
75
- if (!(contentType == null ? void 0 : contentType.includes("text/event-stream"))) {
76
- throw new Error("Response is not a Server-Sent Events stream");
77
- }
78
- (_a = handlers.onOpen) == null ? void 0 : _a.call(handlers);
79
- const reader = (_b = response.body) == null ? void 0 : _b.getReader();
80
- const decoder = new TextDecoder();
81
- let buffer = "";
82
- if (!reader) {
83
- throw new Error("Response body is null");
84
- }
85
- while (true) {
86
- const { done, value } = yield reader.read();
87
- if (done) {
88
- (_c = handlers.onClose) == null ? void 0 : _c.call(handlers);
89
- break;
90
- }
91
- buffer += decoder.decode(value, { stream: true });
92
- const lines = buffer.split("\n\n");
93
- buffer = lines.pop() || "";
94
- for (const line of lines) {
95
- if (line.trim()) {
96
- const message = this.parseMessage(line);
97
- (_d = handlers.onMessage) == null ? void 0 : _d.call(handlers, message);
98
- }
99
- }
100
- }
101
- } catch (error) {
102
- if (this.timeoutTimer) {
103
- clearTimeout(this.timeoutTimer);
104
- this.timeoutTimer = null;
105
- }
106
- if (this.controller && !this.controller.signal.aborted) {
107
- (_e = handlers.onError) == null ? void 0 : _e.call(handlers, error);
108
- }
58
+ },
59
+ onmessage(msg) {
60
+ var _a2;
61
+ (_a2 = handlers.onMessage) == null ? void 0 : _a2.call(handlers, msg);
62
+ },
63
+ onclose() {
64
+ var _a2;
65
+ (_a2 = handlers.onClose) == null ? void 0 : _a2.call(handlers);
66
+ },
67
+ onerror(err) {
68
+ var _a2;
69
+ (_a2 = handlers.onError) == null ? void 0 : _a2.call(handlers, err);
70
+ throw err;
109
71
  }
110
72
  });
111
- }
112
- /**
113
- * 使用 EventSource 连接(仅支持 GET 请求)
114
- */
115
- connect(url, handlers) {
116
- this.eventSource = new EventSource(url);
117
- this.eventSource.onopen = () => {
118
- var _a;
119
- (_a = handlers.onOpen) == null ? void 0 : _a.call(handlers);
120
- };
121
- this.eventSource.onmessage = (event) => {
122
- var _a;
123
- (_a = handlers.onMessage) == null ? void 0 : _a.call(handlers, {
124
- data: event.data,
125
- event: event.type
126
- });
127
- };
128
- this.eventSource.onerror = (error) => {
129
- var _a;
130
- (_a = handlers.onError) == null ? void 0 : _a.call(handlers, new Error("EventSource error"));
131
- this.close();
132
- };
133
- }
134
- /**
135
- * 解析 SSE 消息
136
- */
137
- parseMessage(line) {
138
- const message = {
139
- data: ""
140
- };
141
- const lines = line.split("\n");
142
- for (const l of lines) {
143
- if (l.startsWith("data: ")) {
144
- message.data = l.slice(6);
145
- } else if (l.startsWith("event: ")) {
146
- message.event = l.slice(7);
147
- } else if (l.startsWith("id: ")) {
148
- message.id = l.slice(4);
149
- } else if (l.startsWith("retry: ")) {
150
- message.retry = parseInt(l.slice(7));
151
- }
152
- }
153
- return message;
154
- }
155
- /**
156
- * 关闭连接
157
- */
158
- close() {
159
- if (this.controller) {
160
- this.controller.abort();
161
- this.controller = null;
162
- }
163
- if (this.eventSource) {
164
- this.eventSource.close();
165
- this.eventSource = null;
166
- }
167
- if (this.timeoutTimer) {
168
- clearTimeout(this.timeoutTimer);
169
- this.timeoutTimer = null;
170
- }
171
- }
172
- }
173
- function sse(config, handlers) {
174
- const connection = new SSEConnection();
175
- if (config.method === "POST" || config.timeout) {
176
- connection.request(config, handlers);
177
- } else {
178
- connection.connect(config.url, handlers);
179
- }
180
- return connection;
73
+ });
181
74
  }
182
75
  export {
183
- SSEConnection,
184
76
  sse
185
77
  };
186
78
  //# sourceMappingURL=sse.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse.js","sources":["../../src/sse.ts"],"sourcesContent":["/**\r\n * SSE (Server-Sent Events) 请求配置\r\n */\r\nexport interface SSEConfig {\r\n /** 请求 URL */\r\n url: string\r\n /** 请求方法 */\r\n method?: 'GET' | 'POST'\r\n /** 请求头 */\r\n headers?: Record<string, string>\r\n /** 请求体(POST 请求时使用) */\r\n body?: any\r\n /** 请求超时时间(毫秒) */\r\n timeout?: number\r\n}\r\n\r\n/**\r\n * SSE 消息\r\n */\r\nexport interface SSEMessage {\r\n /** 数据内容 */\r\n data: string\r\n /** 事件类型 */\r\n event?: string\r\n /** ID */\r\n id?: string\r\n /** 重连时间(毫秒) */\r\n retry?: number\r\n}\r\n\r\n/**\r\n * SSE 事件处理器\r\n */\r\nexport interface SSEHandlers {\r\n /** 接收到消息时触发 */\r\n onMessage?: (message: SSEMessage) => void\r\n /** 连接打开时触发 */\r\n onOpen?: () => void\r\n /** 连接关闭时触发 */\r\n onClose?: () => void\r\n /** 发生错误时触发 */\r\n onError?: (error: Error) => void\r\n}\r\n\r\n/**\r\n * SSE 连接类\r\n */\r\nexport class SSEConnection {\r\n private controller: AbortController | null = null\r\n private eventSource: EventSource | null = null\r\n private timeoutTimer: NodeJS.Timeout | null = null\r\n\r\n /**\r\n * 发起 SSE 请求\r\n */\r\n async request(config: SSEConfig, handlers: SSEHandlers): Promise<void> {\r\n const { url, method = 'GET', headers = {}, body, timeout = 30000 } = config\r\n\r\n // 使用 AbortController 支持取消请求\r\n this.controller = new AbortController()\r\n\r\n try {\r\n // 设置超时\r\n if (timeout > 0) {\r\n this.timeoutTimer = setTimeout(() => {\r\n this.close()\r\n handlers.onError?.(new Error(`Request timeout after ${timeout}ms`))\r\n }, timeout)\r\n }\r\n\r\n // 发起请求\r\n const response = await fetch(url, {\r\n method,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...headers\r\n },\r\n body: method === 'POST' ? JSON.stringify(body) : undefined,\r\n signal: this.controller.signal\r\n })\r\n\r\n // 清除超时定时器\r\n if (this.timeoutTimer) {\r\n clearTimeout(this.timeoutTimer)\r\n this.timeoutTimer = null\r\n }\r\n\r\n if (!response.ok) {\r\n throw new Error(`HTTP error! status: ${response.status}`)\r\n }\r\n\r\n // 检查响应是否为 SSE 流\r\n const contentType = response.headers.get('content-type')\r\n if (!contentType?.includes('text/event-stream')) {\r\n throw new Error('Response is not a Server-Sent Events stream')\r\n }\r\n\r\n // 触发打开事件\r\n handlers.onOpen?.()\r\n\r\n // 读取流\r\n const reader = response.body?.getReader()\r\n const decoder = new TextDecoder()\r\n let buffer = ''\r\n\r\n if (!reader) {\r\n throw new Error('Response body is null')\r\n }\r\n\r\n while (true) {\r\n const { done, value } = await reader.read()\r\n\r\n if (done) {\r\n handlers.onClose?.()\r\n break\r\n }\r\n\r\n // 解码数据并添加到缓冲区\r\n buffer += decoder.decode(value, { stream: true })\r\n\r\n // 处理完整的消息\r\n const lines = buffer.split('\\n\\n')\r\n buffer = lines.pop() || ''\r\n\r\n for (const line of lines) {\r\n if (line.trim()) {\r\n const message = this.parseMessage(line)\r\n handlers.onMessage?.(message)\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n // 清除超时定时器\r\n if (this.timeoutTimer) {\r\n clearTimeout(this.timeoutTimer)\r\n this.timeoutTimer = null\r\n }\r\n\r\n // 如果不是主动取消,触发错误事件\r\n if (this.controller && !this.controller.signal.aborted) {\r\n handlers.onError?.(error as Error)\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * 使用 EventSource 连接(仅支持 GET 请求)\r\n */\r\n connect(url: string, handlers: SSEHandlers): void {\r\n this.eventSource = new EventSource(url)\r\n\r\n this.eventSource.onopen = () => {\r\n handlers.onOpen?.()\r\n }\r\n\r\n this.eventSource.onmessage = event => {\r\n handlers.onMessage?.({\r\n data: event.data,\r\n event: event.type\r\n })\r\n }\r\n\r\n this.eventSource.onerror = error => {\r\n handlers.onError?.(new Error('EventSource error'))\r\n this.close()\r\n }\r\n }\r\n\r\n /**\r\n * 解析 SSE 消息\r\n */\r\n private parseMessage(line: string): SSEMessage {\r\n const message: SSEMessage = {\r\n data: ''\r\n }\r\n\r\n const lines = line.split('\\n')\r\n for (const l of lines) {\r\n if (l.startsWith('data: ')) {\r\n message.data = l.slice(6)\r\n } else if (l.startsWith('event: ')) {\r\n message.event = l.slice(7)\r\n } else if (l.startsWith('id: ')) {\r\n message.id = l.slice(4)\r\n } else if (l.startsWith('retry: ')) {\r\n message.retry = parseInt(l.slice(7))\r\n }\r\n }\r\n\r\n return message\r\n }\r\n\r\n /**\r\n * 关闭连接\r\n */\r\n close(): void {\r\n // 取消 fetch 请求\r\n if (this.controller) {\r\n this.controller.abort()\r\n this.controller = null\r\n }\r\n\r\n // 关闭 EventSource\r\n if (this.eventSource) {\r\n this.eventSource.close()\r\n this.eventSource = null\r\n }\r\n\r\n // 清除超时定时器\r\n if (this.timeoutTimer) {\r\n clearTimeout(this.timeoutTimer)\r\n this.timeoutTimer = null\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * 发起 SSE 请求(便捷方法)\r\n */\r\nexport function sse(config: SSEConfig, handlers: SSEHandlers): SSEConnection {\r\n const connection = new SSEConnection()\r\n\r\n // POST 请求或自定义配置使用 fetch\r\n if (config.method === 'POST' || config.timeout) {\r\n connection.request(config, handlers)\r\n } else {\r\n // GET 请求使用 EventSource\r\n connection.connect(config.url, handlers)\r\n }\r\n\r\n return connection\r\n}\r\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"sse.js","sources":["../../src/sse.ts"],"sourcesContent":["import { fetchEventSource, type EventSourceMessage } from '@microsoft/fetch-event-source'\r\n\r\n// 重新导出 EventSourceMessage 类型\r\nexport type { EventSourceMessage }\r\n\r\n/**\r\n * SSE 请求配置\r\n */\r\nexport interface SSEConfig {\r\n /** 请求 URL */\r\n url: string\r\n /** 请求方法(默认 'POST') */\r\n method?: 'GET' | 'POST'\r\n /** 请求头 */\r\n headers?: Record<string, string>\r\n /** 请求体(对象类型) */\r\n body?: object\r\n /** AbortController 的 signal(用于取消请求) */\r\n signal?: AbortSignal\r\n /** 认证 Token */\r\n token?: string\r\n /** 页面隐藏时是否保持连接(默认 true) */\r\n openWhenHidden?: boolean\r\n}\r\n\r\n/**\r\n * SSE 事件处理器\r\n */\r\nexport interface SSEHandlers {\r\n /** 连接打开时触发(可验证响应) */\r\n onOpen?: (response: Response) => void | Promise<void>;\r\n /** 接收到消息时触发 */\r\n onMessage?: (message: EventSourceMessage) => void;\r\n /** 连接关闭时触发 */\r\n onClose?: () => void;\r\n /** 发生错误时触发 */\r\n onError?: (error: Error) => void;\r\n}\r\n\r\n/**\r\n * 发起 SSE 请求\r\n *\r\n * @example\r\n * ```ts\r\n * // 基础用法\r\n * await sse({\r\n * url: '/api/stream',\r\n * body: { prompt: 'hello' }\r\n * }, {\r\n * onMessage(msg) {\r\n * console.log(msg.data)\r\n * }\r\n * })\r\n *\r\n * // 使用 Token 认证\r\n * await sse({\r\n * url: '/api/stream',\r\n * token: 'Bearer your-token',\r\n * body: { prompt: 'hello' }\r\n * }, {\r\n * onOpen(response) {\r\n * console.log('连接已打开')\r\n * },\r\n * onMessage(msg) {\r\n * console.log(msg.data)\r\n * },\r\n * onError(err) {\r\n * console.error(err)\r\n * }\r\n * })\r\n * ```\r\n */\r\nexport async function sse(\r\n config: SSEConfig,\r\n handlers: SSEHandlers\r\n): Promise<void> {\r\n const { url, method = 'POST', headers = {}, body, signal, token, openWhenHidden = true } = config;\r\n\r\n // 构建请求头,自动添加 Authorization\r\n const requestHeaders: Record<string, string> = {\r\n 'Content-Type': 'application/json;charset=utf-8',\r\n ...headers\r\n };\r\n\r\n if (token) {\r\n requestHeaders['Authorization'] = token;\r\n }\r\n\r\n await fetchEventSource(url, {\r\n method,\r\n headers: requestHeaders,\r\n body: JSON.stringify(body),\r\n signal,\r\n openWhenHidden,\r\n\r\n async onopen(response) {\r\n await handlers.onOpen?.(response);\r\n },\r\n\r\n onmessage(msg) {\r\n handlers.onMessage?.(msg);\r\n },\r\n\r\n onclose() {\r\n handlers.onClose?.();\r\n },\r\n\r\n onerror(err) {\r\n handlers.onError?.(err);\r\n throw err;\r\n }\r\n });\r\n}\r\n"],"names":["_a"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAsB,IACpB,QACA,UACe;AAAA;AACf,UAAM,EAAE,KAAK,SAAS,QAAQ,UAAU,CAAA,GAAI,MAAM,QAAQ,OAAO,iBAAiB,KAAA,IAAS;AAG3F,UAAM,iBAAyC;AAAA,MAC7C,gBAAgB;AAAA,OACb;AAGL,QAAI,OAAO;AACT,qBAAe,eAAe,IAAI;AAAA,IACpC;AAEA,UAAM,iBAAiB,KAAK;AAAA,MAC1B;AAAA,MACA,SAAS;AAAA,MACT,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB;AAAA,MACA;AAAA,MAEM,OAAO,UAAU;AAAA;;AACrB,iBAAMA,MAAA,SAAS,WAAT,gBAAAA,IAAA,eAAkB;AAAA,QAC1B;AAAA;AAAA,MAEA,UAAU,KAAK;;AACb,SAAAA,MAAA,SAAS,cAAT,gBAAAA,IAAA,eAAqB;AAAA,MACvB;AAAA,MAEA,UAAU;;AACR,SAAAA,MAAA,SAAS,YAAT,gBAAAA,IAAA;AAAA,MACF;AAAA,MAEA,QAAQ,KAAK;;AACX,SAAAA,MAAA,SAAS,YAAT,gBAAAA,IAAA,eAAmB;AACnB,cAAM;AAAA,MACR;AAAA,IAAA,CACD;AAAA,EACH;AAAA;"}
@@ -1,153 +1,34 @@
1
1
  function extractThinking(text, config = {}) {
2
- const { tagName = "think", includeTags = false, removeRest = false } = config;
2
+ const { tagName = "think", mode = "full" } = config;
3
3
  const openTag = `<${tagName}>`;
4
4
  const closeTag = `</${tagName}>`;
5
5
  const openIndex = text.indexOf(openTag);
6
- const closeIndex = text.indexOf(closeTag);
7
- if (openIndex === -1 || closeIndex === -1) {
6
+ const closeTagIndex = text.indexOf(closeTag);
7
+ if (mode === "end-tag-only" && closeTagIndex !== -1) {
8
+ const thinking = text.slice(0, closeTagIndex).trim();
9
+ const content = text.slice(closeTagIndex + closeTag.length).trim();
8
10
  return {
9
- thinking: "",
10
- rest: text,
11
- hasThinking: false
11
+ thinking,
12
+ content
12
13
  };
13
14
  }
14
- const thinkingStart = openIndex + openTag.length;
15
- const thinkingEnd = closeIndex;
16
- const thinkingContent = text.slice(thinkingStart, thinkingEnd);
17
- const beforeContent = text.slice(0, openIndex);
18
- const afterContent = text.slice(closeIndex + closeTag.length);
19
- const restContent = beforeContent + afterContent;
20
- return {
21
- thinking: includeTags ? `${openTag}${thinkingContent}${closeTag}` : thinkingContent,
22
- rest: removeRest ? "" : restContent,
23
- hasThinking: true
24
- };
25
- }
26
- class ThinkingStreamExtractor {
27
- constructor(config = {}) {
28
- this.isComplete = false;
29
- this.config = {
30
- tagName: config.tagName || "think",
31
- includeTags: config.includeTags || false,
32
- removeRest: config.removeRest || false
33
- };
34
- this.state = {
35
- inThinking: false,
36
- tagBuffer: "",
37
- contentBuffer: "",
38
- beforeContent: "",
39
- afterContent: ""
40
- };
41
- }
42
- /**
43
- * 追加文本并提取思考内容
44
- */
45
- append(text) {
46
- if (this.isComplete) {
47
- return {
48
- thinking: this.getThinking(),
49
- rest: this.getRest(),
50
- isComplete: true
51
- };
52
- }
53
- const { tagName } = this.config;
54
- const openTag = `<${tagName}>`;
55
- const closeTag = `</${tagName}>`;
56
- for (let i = 0; i < text.length; i++) {
57
- const char = text[i];
58
- if (!this.state.inThinking) {
59
- this.state.tagBuffer += char;
60
- if (this.state.tagBuffer.endsWith(openTag)) {
61
- const tagStart = this.state.tagBuffer.length - openTag.length;
62
- this.state.beforeContent += this.state.tagBuffer.slice(0, tagStart);
63
- this.state.tagBuffer = "";
64
- this.state.inThinking = true;
65
- continue;
66
- }
67
- if (this.state.tagBuffer.length > openTag.length) {
68
- this.state.beforeContent += this.state.tagBuffer.slice(0, 1);
69
- this.state.tagBuffer = this.state.tagBuffer.slice(1);
70
- }
71
- } else {
72
- this.state.tagBuffer += char;
73
- if (this.state.tagBuffer.endsWith(closeTag)) {
74
- const tagEnd = this.state.tagBuffer.length - closeTag.length;
75
- this.state.contentBuffer += this.state.tagBuffer.slice(0, tagEnd);
76
- this.state.tagBuffer = "";
77
- this.state.inThinking = false;
78
- this.isComplete = true;
79
- break;
80
- }
81
- if (this.state.tagBuffer.length > closeTag.length) {
82
- const overflow = this.state.tagBuffer.slice(0, 1);
83
- this.state.contentBuffer += overflow;
84
- this.state.tagBuffer = this.state.tagBuffer.slice(1);
85
- }
86
- }
87
- }
88
- if (!this.state.inThinking && this.state.tagBuffer) {
89
- if (this.isComplete) {
90
- this.state.afterContent += this.state.tagBuffer;
91
- } else {
92
- this.state.beforeContent += this.state.tagBuffer;
93
- }
94
- this.state.tagBuffer = "";
95
- }
15
+ if (openIndex !== -1 && closeTagIndex !== -1) {
16
+ const thinkingStart = openIndex + openTag.length;
17
+ const thinkingContent = text.slice(thinkingStart, closeTagIndex).trim();
18
+ const beforeContent = text.slice(0, openIndex);
19
+ const afterContent = text.slice(closeTagIndex + closeTag.length);
20
+ const content = (beforeContent + afterContent).trim();
96
21
  return {
97
- thinking: this.getThinking(),
98
- rest: this.getRest(),
99
- isComplete: this.isComplete
100
- };
101
- }
102
- /**
103
- * 获取当前提取的思考内容
104
- */
105
- getThinking() {
106
- const { tagName, includeTags } = this.config;
107
- const openTag = `<${tagName}>`;
108
- const closeTag = `</${tagName}>`;
109
- const content = this.state.contentBuffer;
110
- if (includeTags) {
111
- return this.state.inThinking ? `${openTag}${content}` : `${openTag}${content}${closeTag}`;
112
- }
113
- return content;
114
- }
115
- /**
116
- * 获取剩余内容
117
- */
118
- getRest() {
119
- const { removeRest } = this.config;
120
- if (removeRest) {
121
- return "";
122
- }
123
- return this.state.beforeContent + this.state.afterContent;
124
- }
125
- /**
126
- * 是否已完成提取
127
- */
128
- complete() {
129
- return this.isComplete;
130
- }
131
- /**
132
- * 重置提取器
133
- */
134
- reset() {
135
- this.state = {
136
- inThinking: false,
137
- tagBuffer: "",
138
- contentBuffer: "",
139
- beforeContent: "",
140
- afterContent: ""
22
+ thinking: thinkingContent,
23
+ content
141
24
  };
142
- this.isComplete = false;
143
25
  }
144
- }
145
- function createThinkingExtractor(config) {
146
- return new ThinkingStreamExtractor(config);
26
+ return {
27
+ thinking: "",
28
+ content: text.trim()
29
+ };
147
30
  }
148
31
  export {
149
- ThinkingStreamExtractor,
150
- createThinkingExtractor,
151
32
  extractThinking
152
33
  };
153
34
  //# sourceMappingURL=thinking.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"thinking.js","sources":["../../src/thinking.ts"],"sourcesContent":["/**\r\n * 思考过程提取配置\r\n */\r\nexport interface ThinkingExtractConfig {\r\n /** 思考标签名称(默认为 'think') */\r\n tagName?: string\r\n /** 是否提取完整标签内容(包含标签本身) */\r\n includeTags?: boolean\r\n /** 是否移除思考内容后的其余文本 */\r\n removeRest?: boolean\r\n}\r\n\r\n/**\r\n * 思考过程结果\r\n */\r\nexport interface ThinkingResult {\r\n /** 提取的思考内容 */\r\n thinking: string\r\n /** 移除思考后的剩余内容 */\r\n rest: string\r\n /** 是否找到思考内容 */\r\n hasThinking: boolean\r\n}\r\n\r\n/**\r\n * 思考标签匹配状态\r\n */\r\ninterface TagMatchState {\r\n /** 是否在思考标签内 */\r\n inThinking: boolean\r\n /** 已找到的标签部分 */\r\n tagBuffer: string\r\n /** 思考内容缓冲区 */\r\n contentBuffer: string\r\n /** 前置内容 */\r\n beforeContent: string\r\n /** 后置内容 */\r\n afterContent: string\r\n}\r\n\r\n/**\r\n * AI 响应中提取思考过程(完整模式)\r\n */\r\nexport function extractThinking(\r\n text: string,\r\n config: ThinkingExtractConfig = {}\r\n): ThinkingResult {\r\n const { tagName = 'think', includeTags = false, removeRest = false } = config\r\n\r\n const openTag = `<${tagName}>`\r\n const closeTag = `</${tagName}>`\r\n\r\n const openIndex = text.indexOf(openTag)\r\n const closeIndex = text.indexOf(closeTag)\r\n\r\n // 没有找到思考标签\r\n if (openIndex === -1 || closeIndex === -1) {\r\n return {\r\n thinking: '',\r\n rest: text,\r\n hasThinking: false\r\n }\r\n }\r\n\r\n // 提取思考内容\r\n const thinkingStart = openIndex + openTag.length\r\n const thinkingEnd = closeIndex\r\n const thinkingContent = text.slice(thinkingStart, thinkingEnd)\r\n\r\n // 提取剩余内容\r\n const beforeContent = text.slice(0, openIndex)\r\n const afterContent = text.slice(closeIndex + closeTag.length)\r\n const restContent = beforeContent + afterContent\r\n\r\n // 返回结果\r\n return {\r\n thinking: includeTags\r\n ? `${openTag}${thinkingContent}${closeTag}`\r\n : thinkingContent,\r\n rest: removeRest ? '' : restContent,\r\n hasThinking: true\r\n }\r\n}\r\n\r\n/**\r\n * 流式提取思考过程(用于 SSE 流式响应)\r\n */\r\nexport class ThinkingStreamExtractor {\r\n private config: ThinkingExtractConfig\r\n private state: TagMatchState\r\n private isComplete: boolean = false\r\n\r\n constructor(config: ThinkingExtractConfig = {}) {\r\n this.config = {\r\n tagName: config.tagName || 'think',\r\n includeTags: config.includeTags || false,\r\n removeRest: config.removeRest || false\r\n }\r\n this.state = {\r\n inThinking: false,\r\n tagBuffer: '',\r\n contentBuffer: '',\r\n beforeContent: '',\r\n afterContent: ''\r\n }\r\n }\r\n\r\n /**\r\n * 追加文本并提取思考内容\r\n */\r\n append(text: string): { thinking: string; rest: string; isComplete: boolean } {\r\n if (this.isComplete) {\r\n return {\r\n thinking: this.getThinking(),\r\n rest: this.getRest(),\r\n isComplete: true\r\n }\r\n }\r\n\r\n const { tagName } = this.config\r\n const openTag = `<${tagName}>`\r\n const closeTag = `</${tagName}>`\r\n\r\n // 逐字符处理\r\n for (let i = 0; i < text.length; i++) {\r\n const char = text[i]\r\n\r\n if (!this.state.inThinking) {\r\n // 查找开始标签\r\n this.state.tagBuffer += char\r\n\r\n // 检查是否匹配到开始标签\r\n if (this.state.tagBuffer.endsWith(openTag)) {\r\n // 移除已匹配的标签部分\r\n const tagStart = this.state.tagBuffer.length - openTag.length\r\n this.state.beforeContent += this.state.tagBuffer.slice(0, tagStart)\r\n this.state.tagBuffer = ''\r\n this.state.inThinking = true\r\n continue\r\n }\r\n\r\n // 如果缓冲区过长,移除不匹配的部分\r\n if (this.state.tagBuffer.length > openTag.length) {\r\n this.state.beforeContent += this.state.tagBuffer.slice(0, 1)\r\n this.state.tagBuffer = this.state.tagBuffer.slice(1)\r\n }\r\n } else {\r\n // 查找结束标签\r\n this.state.tagBuffer += char\r\n\r\n // 检查是否匹配到结束标签\r\n if (this.state.tagBuffer.endsWith(closeTag)) {\r\n // 保存思考内容(不包含结束标签)\r\n const tagEnd = this.state.tagBuffer.length - closeTag.length\r\n this.state.contentBuffer += this.state.tagBuffer.slice(0, tagEnd)\r\n this.state.tagBuffer = ''\r\n this.state.inThinking = false\r\n this.isComplete = true\r\n break\r\n }\r\n\r\n // 如果缓冲区过长,将内容移到思考缓冲区\r\n if (this.state.tagBuffer.length > closeTag.length) {\r\n const overflow = this.state.tagBuffer.slice(0, 1)\r\n this.state.contentBuffer += overflow\r\n this.state.tagBuffer = this.state.tagBuffer.slice(1)\r\n }\r\n }\r\n }\r\n\r\n // 如果不在思考标签内,将缓冲区内容移到前置/后置内容\r\n if (!this.state.inThinking && this.state.tagBuffer) {\r\n if (this.isComplete) {\r\n this.state.afterContent += this.state.tagBuffer\r\n } else {\r\n this.state.beforeContent += this.state.tagBuffer\r\n }\r\n this.state.tagBuffer = ''\r\n }\r\n\r\n return {\r\n thinking: this.getThinking(),\r\n rest: this.getRest(),\r\n isComplete: this.isComplete\r\n }\r\n }\r\n\r\n /**\r\n * 获取当前提取的思考内容\r\n */\r\n getThinking(): string {\r\n const { tagName, includeTags } = this.config\r\n const openTag = `<${tagName}>`\r\n const closeTag = `</${tagName}>`\r\n\r\n const content = this.state.contentBuffer\r\n\r\n if (includeTags) {\r\n // 如果还在标签内,不添加闭合标签\r\n return this.state.inThinking\r\n ? `${openTag}${content}`\r\n : `${openTag}${content}${closeTag}`\r\n }\r\n\r\n return content\r\n }\r\n\r\n /**\r\n * 获取剩余内容\r\n */\r\n getRest(): string {\r\n const { removeRest } = this.config\r\n if (removeRest) {\r\n return ''\r\n }\r\n return this.state.beforeContent + this.state.afterContent\r\n }\r\n\r\n /**\r\n * 是否已完成提取\r\n */\r\n complete(): boolean {\r\n return this.isComplete\r\n }\r\n\r\n /**\r\n * 重置提取器\r\n */\r\n reset(): void {\r\n this.state = {\r\n inThinking: false,\r\n tagBuffer: '',\r\n contentBuffer: '',\r\n beforeContent: '',\r\n afterContent: ''\r\n }\r\n this.isComplete = false\r\n }\r\n}\r\n\r\n/**\r\n * 创建流式提取器实例\r\n */\r\nexport function createThinkingExtractor(\r\n config?: ThinkingExtractConfig\r\n): ThinkingStreamExtractor {\r\n return new ThinkingStreamExtractor(config)\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"thinking.js","sources":["../../src/thinking.ts"],"sourcesContent":["/**\r\n * 深度思考提取配置\r\n */\r\nexport interface ThinkingExtractConfig {\r\n /** 思考标签名称(默认为 'think') */\r\n tagName?: string\r\n /** 解析模式:'full'(完整标签,默认)| 'end-tag-only'(仅结束标签) */\r\n mode?: 'full' | 'end-tag-only'\r\n}\r\n\r\n/**\r\n * 深度思考提取结果\r\n */\r\nexport interface ThinkingResult {\r\n /** 提取的思考内容 */\r\n thinking: string\r\n /** 实际回答内容 */\r\n content: string\r\n}\r\n\r\n/**\r\n * 从大模型响应中提取深度思考\r\n *\r\n * @example\r\n * ```ts\r\n * // 默认模式:完整标签\r\n * const text1 = '这是回答内容'\r\n * const result1 = extractThinking(text1)\r\n * // { thinking: '这是思考内容', content: '这是回答内容' }\r\n *\r\n * // end-tag-only 模式:只有结束标签\r\n * const text2 = '这是前面的思考这是后面的正文'\r\n * const result2 = extractThinking(text2, { mode: 'end-tag-only' })\r\n * // { thinking: '这是前面的思考', content: '这是后面的正文' }\r\n *\r\n * // 没有标签时\r\n * const text3 = '全部是正文内容'\r\n * const result3 = extractThinking(text3)\r\n * // { thinking: '', content: '全部是正文内容' }\r\n * ```\r\n */\r\nexport function extractThinking(\r\n text: string,\r\n config: ThinkingExtractConfig = {}\r\n): ThinkingResult {\r\n const { tagName = 'think', mode = 'full' } = config\r\n\r\n const openTag = `<${tagName}>`\r\n const closeTag = `</${tagName}>`\r\n\r\n const openIndex = text.indexOf(openTag)\r\n const closeTagIndex = text.indexOf(closeTag)\r\n\r\n // end-tag-only 模式:只有结束标签\r\n if (mode === 'end-tag-only' && closeTagIndex !== -1) {\r\n const thinking = text.slice(0, closeTagIndex).trim()\r\n const content = text.slice(closeTagIndex + closeTag.length).trim()\r\n\r\n return {\r\n thinking: thinking,\r\n content: content\r\n }\r\n }\r\n\r\n // full 模式(默认):完整标签 内容\r\n if (openIndex !== -1 && closeTagIndex !== -1) {\r\n const thinkingStart = openIndex + openTag.length\r\n const thinkingContent = text.slice(thinkingStart, closeTagIndex).trim()\r\n\r\n const beforeContent = text.slice(0, openIndex)\r\n const afterContent = text.slice(closeTagIndex + closeTag.length)\r\n const content = (beforeContent + afterContent).trim()\r\n\r\n return {\r\n thinking: thinkingContent,\r\n content: content\r\n }\r\n }\r\n\r\n // 没有找到标签,全部是正文\r\n return {\r\n thinking: '',\r\n content: text.trim()\r\n }\r\n}\r\n"],"names":[],"mappings":"AAyCO,SAAS,gBACd,MACA,SAAgC,IAChB;AAChB,QAAM,EAAE,UAAU,SAAS,OAAO,WAAW;AAE7C,QAAM,UAAU,IAAI,OAAO;AAC3B,QAAM,WAAW,KAAK,OAAO;AAE7B,QAAM,YAAY,KAAK,QAAQ,OAAO;AACtC,QAAM,gBAAgB,KAAK,QAAQ,QAAQ;AAG3C,MAAI,SAAS,kBAAkB,kBAAkB,IAAI;AACnD,UAAM,WAAW,KAAK,MAAM,GAAG,aAAa,EAAE,KAAA;AAC9C,UAAM,UAAU,KAAK,MAAM,gBAAgB,SAAS,MAAM,EAAE,KAAA;AAE5D,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,cAAc,MAAM,kBAAkB,IAAI;AAC5C,UAAM,gBAAgB,YAAY,QAAQ;AAC1C,UAAM,kBAAkB,KAAK,MAAM,eAAe,aAAa,EAAE,KAAA;AAEjE,UAAM,gBAAgB,KAAK,MAAM,GAAG,SAAS;AAC7C,UAAM,eAAe,KAAK,MAAM,gBAAgB,SAAS,MAAM;AAC/D,UAAM,WAAW,gBAAgB,cAAc,KAAA;AAE/C,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,IAAA;AAAA,EAEJ;AAGA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,SAAS,KAAK,KAAA;AAAA,EAAK;AAEvB;"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __async = (__this, __arguments, generator) => {
3
+ return new Promise((resolve, reject) => {
4
+ var fulfilled = (value) => {
5
+ try {
6
+ step(generator.next(value));
7
+ } catch (e) {
8
+ reject(e);
9
+ }
10
+ };
11
+ var rejected = (value) => {
12
+ try {
13
+ step(generator.throw(value));
14
+ } catch (e) {
15
+ reject(e);
16
+ }
17
+ };
18
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
19
+ step((generator = generator.apply(__this, __arguments)).next());
20
+ });
21
+ };
22
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
23
+ function copyToClipboard(text) {
24
+ return __async(this, null, function* () {
25
+ if (typeof window === "undefined") return false;
26
+ if (navigator.clipboard && navigator.clipboard.writeText) {
27
+ try {
28
+ yield navigator.clipboard.writeText(text);
29
+ return true;
30
+ } catch (error) {
31
+ console.error("复制失败:", error);
32
+ return false;
33
+ }
34
+ }
35
+ const textArea = document.createElement("textarea");
36
+ textArea.value = text;
37
+ textArea.style.position = "fixed";
38
+ textArea.style.opacity = "0";
39
+ textArea.style.left = "-9999px";
40
+ document.body.appendChild(textArea);
41
+ textArea.focus();
42
+ textArea.select();
43
+ try {
44
+ const successful = document.execCommand("copy");
45
+ document.body.removeChild(textArea);
46
+ return successful;
47
+ } catch (error) {
48
+ console.error("复制失败:", error);
49
+ document.body.removeChild(textArea);
50
+ return false;
51
+ }
52
+ });
53
+ }
54
+ function isDom(val) {
55
+ return val instanceof HTMLElement || val instanceof Document;
56
+ }
57
+ exports.copyToClipboard = copyToClipboard;
58
+ exports.isDom = isDom;
59
+ //# sourceMappingURL=helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.js","sources":["../../src/helper.ts"],"sourcesContent":["/**\r\n * 复制文本到剪贴板\r\n *\r\n * @param text - 要复制的文本\r\n * @returns Promise<boolean> - 成功返回 true,失败返回 false\r\n *\r\n * @example\r\n * ```typescript\r\n * import { copyToClipboard } from '@will1123/lx-ui-utils'\r\n *\r\n * // 基础用法\r\n * const success = await copyToClipboard('Hello World')\r\n * if (success) {\r\n * console.log('复制成功')\r\n * } else {\r\n * console.log('复制失败')\r\n * }\r\n *\r\n * // 复制代码\r\n * const code = 'console.log(\"Hello World\")'\r\n * await copyToClipboard(code)\r\n *\r\n * // 复制 HTML 内容\r\n * const html = '<div>Hello</div>'\r\n * await copyToClipboard(html)\r\n * ```\r\n */\r\nexport async function copyToClipboard(text: string): Promise<boolean> {\r\n if (typeof window === 'undefined') return false;\r\n\r\n // 使用现代 Clipboard API\r\n if (navigator.clipboard && navigator.clipboard.writeText) {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n return true;\r\n } catch (error) {\r\n console.error('复制失败:', error);\r\n return false;\r\n }\r\n }\r\n\r\n // 降级方案:使用 document.execCommand\r\n const textArea = document.createElement('textarea');\r\n textArea.value = text;\r\n textArea.style.position = 'fixed';\r\n textArea.style.opacity = '0';\r\n textArea.style.left = '-9999px';\r\n document.body.appendChild(textArea);\r\n textArea.focus();\r\n textArea.select();\r\n\r\n try {\r\n const successful = document.execCommand('copy');\r\n document.body.removeChild(textArea);\r\n return successful;\r\n } catch (error) {\r\n console.error('复制失败:', error);\r\n document.body.removeChild(textArea);\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * 判断值是否为 DOM 元素(HTMLElement 或 Document)\r\n *\r\n * @param val - 要判断的值\r\n * @returns 是否为 DOM 元素\r\n *\r\n * @example\r\n * ```typescript\r\n * import { isDom } from '@will1123/lx-ui-utils'\r\n *\r\n * isDom(document.body) // true\r\n * isDom(document) // true\r\n * isDom({}) // false\r\n * isDom('div') // false\r\n * ```\r\n */\r\nexport function isDom(val: unknown): val is HTMLElement | Document {\r\n return val instanceof HTMLElement || val instanceof Document;\r\n}\r\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAsB,gBAAgB,MAAgC;AAAA;AACpE,QAAI,OAAO,WAAW,YAAa,QAAO;AAG1C,QAAI,UAAU,aAAa,UAAU,UAAU,WAAW;AACxD,UAAI;AACF,cAAM,UAAU,UAAU,UAAU,IAAI;AACxC,eAAO;AAAA,MACT,SAAS,OAAO;AACd,gBAAQ,MAAM,SAAS,KAAK;AAC5B,eAAO;AAAA,MACT;AAAA,IACF;AAGA,UAAM,WAAW,SAAS,cAAc,UAAU;AAClD,aAAS,QAAQ;AACjB,aAAS,MAAM,WAAW;AAC1B,aAAS,MAAM,UAAU;AACzB,aAAS,MAAM,OAAO;AACtB,aAAS,KAAK,YAAY,QAAQ;AAClC,aAAS,MAAA;AACT,aAAS,OAAA;AAET,QAAI;AACF,YAAM,aAAa,SAAS,YAAY,MAAM;AAC9C,eAAS,KAAK,YAAY,QAAQ;AAClC,aAAO;AAAA,IACT,SAAS,OAAO;AACd,cAAQ,MAAM,SAAS,KAAK;AAC5B,eAAS,KAAK,YAAY,QAAQ;AAClC,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAkBO,SAAS,MAAM,KAA6C;AACjE,SAAO,eAAe,eAAe,eAAe;AACtD;;;"}
package/dist/lib/index.js CHANGED
@@ -3,13 +3,15 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const sse = require("./sse.js");
4
4
  const thinking = require("./thinking.js");
5
5
  const markdown = require("./markdown.js");
6
- exports.SSEConnection = sse.SSEConnection;
6
+ const helper = require("./helper.js");
7
7
  exports.sse = sse.sse;
8
- exports.ThinkingStreamExtractor = thinking.ThinkingStreamExtractor;
9
- exports.createThinkingExtractor = thinking.createThinkingExtractor;
10
8
  exports.extractThinking = thinking.extractThinking;
11
- exports.MarkdownRenderer = markdown.MarkdownRenderer;
12
- exports.MarkdownStreamRenderer = markdown.MarkdownStreamRenderer;
13
- exports.createMarkdownRenderer = markdown.createMarkdownRenderer;
14
- exports.renderMarkdown = markdown.renderMarkdown;
9
+ exports.bindMarkdownCodeBoxEvents = markdown.bindMarkdownCodeBoxEvents;
10
+ exports.createMarkdownRender = markdown.createMarkdownRender;
11
+ exports.getMarkdownCodeTheme = markdown.getMarkdownCodeTheme;
12
+ exports.setMarkdownCodeTheme = markdown.setMarkdownCodeTheme;
13
+ exports.toggleMarkdownCodeTheme = markdown.toggleMarkdownCodeTheme;
14
+ exports.updateMarkdownCodeBlocksTheme = markdown.updateMarkdownCodeBlocksTheme;
15
+ exports.copyToClipboard = helper.copyToClipboard;
16
+ exports.isDom = helper.isDom;
15
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}