fix: system 系统提示词优化

This commit is contained in:
imeepos
2025-07-22 18:57:18 +08:00
parent 3a67bbf52a
commit dd0a4f1470
12 changed files with 11 additions and 1871 deletions

View File

@@ -175,8 +175,8 @@ impl Default for RagGroundingConfig {
data_store_id: "searchable-model-images_1752827560253".to_string(), // 使用存在的数据存储
model_id: "gemini-2.5-flash".to_string(),
temperature: 1.0,
max_output_tokens: 8192,
system_prompt: Some("你是一个短视频情景穿搭分析专家, 根据用户预想的情景输出符合逻辑的情景和模特穿搭描述,必须依据已知的数据返回可能的方案, 并且给出参照的依据;如果没有匹配的数据支持,返回空结果;".to_string()),
max_output_tokens: 1000 * 10,
system_prompt: Some("你是一个短视频情景穿搭分析专家, 根据用户预想的情景检索RAG然后输出符合逻辑的情景和模特穿搭描述,必须依据已知的数据返回可能的方案, 并且给出参照的依据;如果没有匹配的数据支持,返回空结果;".to_string()),
}
}
}

View File

@@ -5,16 +5,11 @@ import {
FileImage,
Clock,
ExternalLink,
Eye,
Play,
Volume2,
Image as ImageIcon,
Calendar,
Hash,
HardDrive
} from 'lucide-react';
import { GroundingSource } from '../types/ragGrounding';
import { MaterialThumbnail } from './MaterialThumbnail';
/**
* 聊天素材卡片属性接口

View File

@@ -5,7 +5,6 @@ import { GroundingMetadata, GroundingSupport, GroundingSource } from '../types/r
import ImageCard from './ImageCard';
import ImagePreviewModal from './ImagePreviewModal';
import useImageDownload from '../hooks/useImageDownload';
import { ReferenceFootnote } from './ReferenceFootnote';
/**
* 文字片段信息
@@ -87,12 +86,12 @@ export const GroundedText: React.FC<GroundedTextProps> = ({
const sources = groundingMetadata.sources;
// 按开始位置排序
const sortedSupports = [...supports].sort((a, b) => a.segment.start_index - b.segment.start_index);
const sortedSupports = [...supports].sort((a, b) => a.segment.endIndex - b.segment.startIndex);
let currentIndex = 0;
for (const support of sortedSupports) {
const { start_index, end_index, text: segmentText } = support.segment;
const { startIndex: start_index, endIndex: end_index, text: segmentText } = support.segment;
// 添加前面的普通文本
if (currentIndex < start_index) {

View File

@@ -173,7 +173,7 @@ export const MarkdownParserRenderer: React.FC<MarkdownParserRendererProps> = ({
onClick={handleNodeClick}
style={{ marginLeft: `${depth * 20}px` }}
>
{node.children.map((child, index) => renderNode(child, depth + 1))}
{node.children.map((child, _index) => renderNode(child, depth + 1))}
{positionInfo}
</p>
);
@@ -237,7 +237,7 @@ export const MarkdownParserRenderer: React.FC<MarkdownParserRendererProps> = ({
onClick={handleNodeClick}
style={{ marginLeft: `${depth * 20}px` }}
>
{node.children.map((child, index) => renderNode(child, depth + 1))}
{node.children.map((child, _index) => renderNode(child, depth + 1))}
{positionInfo}
</ListTag>
);
@@ -250,7 +250,7 @@ export const MarkdownParserRenderer: React.FC<MarkdownParserRendererProps> = ({
onClick={handleNodeClick}
style={{ marginLeft: `${depth * 20}px` }}
>
{node.children.map((child, index) => renderNode(child, depth + 1))}
{node.children.map((child, _index) => renderNode(child, depth + 1))}
{positionInfo}
</li>
);
@@ -262,7 +262,7 @@ export const MarkdownParserRenderer: React.FC<MarkdownParserRendererProps> = ({
className={nodeClasses}
onClick={handleNodeClick}
>
{node.children.map((child, index) => renderNode(child, depth + 1))}
{node.children.map((child, _index) => renderNode(child, depth + 1))}
{positionInfo}
</em>
);
@@ -274,7 +274,7 @@ export const MarkdownParserRenderer: React.FC<MarkdownParserRendererProps> = ({
className={nodeClasses}
onClick={handleNodeClick}
>
{node.children.map((child, index) => renderNode(child, depth + 1))}
{node.children.map((child, _index) => renderNode(child, depth + 1))}
{positionInfo}
</strong>
);
@@ -299,7 +299,7 @@ export const MarkdownParserRenderer: React.FC<MarkdownParserRendererProps> = ({
onClick={handleNodeClick}
style={{ marginLeft: `${depth * 20}px` }}
>
{node.children.map((child, index) => renderNode(child, depth + 1))}
{node.children.map((child, _index) => renderNode(child, depth + 1))}
{positionInfo}
</div>
);

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { describe, test, expect, vi } from 'vitest';

View File

@@ -1,287 +0,0 @@
import React, { useState } from 'react';
import { EnhancedMarkdownRenderer } from '../components/EnhancedMarkdownRenderer';
import { GroundingMetadata } from '../types/ragGrounding';
/**
* EnhancedMarkdownRenderer使用示例
* 展示基于MarkdownService的增强Markdown渲染功能
*/
const EnhancedMarkdownRendererExample: React.FC = () => {
const [content, setContent] = useState(`# 增强Markdown渲染器示例
这是一个基于**Tree-sitter MarkdownService**的增强Markdown渲染器替代了传统的ReactMarkdown。
## 🚀 新功能特性
### 1. 基于Tree-sitter的精确解析
- 使用pulldown-cmark进行语法树解析
- 保留完整的位置信息和原文引用
- 更准确的语法分析和错误恢复
### 2. 实时解析和验证
- 支持实时解析模式
- 文档结构验证
- 解析统计信息显示
### 3. 增强的渲染功能
- 支持所有标准Markdown语法
- 自定义样式和主题
- 错误状态和加载状态处理
## 📝 语法支持
### 文本格式
- **粗体文本**
- *斜体文本*
- \`内联代码\`
- ~~删除线~~(如果支持)
### 链接和图片
- [普通链接](https://example.com)
- [带标题的链接](https://example.com "这是标题")
- ![图片](https://via.placeholder.com/150x100 "示例图片")
### 列表
#### 无序列表
- 项目1
- 项目2
- 子项目2.1
- 子项目2.2
- 项目3
#### 有序列表
1. 第一项
2. 第二项
3. 第三项
### 代码块
\`\`\`typescript
// TypeScript代码示例
interface MarkdownRendererProps {
content: string;
enableMarkdown?: boolean;
showStatistics?: boolean;
}
const renderer = new MarkdownRenderer(props);
renderer.render();
\`\`\`
\`\`\`javascript
// JavaScript代码示例
function parseMarkdown(content) {
return markdownService.parseMarkdown(content);
}
\`\`\`
### 引用块
> 这是一个引用块。
>
> 它可以包含多行内容,并且支持**格式化**文本。
### 表格(如果支持)
| 功能 | 状态 | 描述 |
|------|------|------|
| 解析 | ✅ | Tree-sitter解析 |
| 渲染 | ✅ | React组件渲染 |
| 验证 | ✅ | 文档结构验证 |
---
## 🔧 技术实现
这个渲染器的核心技术栈:
1. **后端解析**: Rust + pulldown-cmark
2. **前端渲染**: React + TypeScript
3. **通信层**: Tauri命令系统
4. **样式**: TailwindCSS
### 性能优势
- 更快的解析速度
- 更低的内存占用
- 更好的错误处理
- 实时解析支持
## 📊 使用统计
当启用统计信息时,您可以看到:
- 解析的节点总数
- 错误节点数量
- 解析耗时
- 文档最大深度
这些信息有助于优化文档结构和性能调试。
`);
const [enableMarkdown, setEnableMarkdown] = useState(true);
const [showStatistics, setShowStatistics] = useState(false);
const [enableRealTimeParsing, setEnableRealTimeParsing] = useState(false);
const [enableReferences, setEnableReferences] = useState(true);
// 模拟grounding metadata
const mockGroundingMetadata: GroundingMetadata = {
sources: [
{
title: 'Markdown语法指南',
uri: 'https://markdown-guide.org',
content: { snippet: 'Markdown是一种轻量级标记语言...' },
},
{
title: 'Tree-sitter文档',
uri: 'https://tree-sitter.github.io',
content: { snippet: 'Tree-sitter是一个解析器生成工具...' },
},
{
title: 'React组件开发',
uri: 'https://react.dev',
content: { snippet: 'React是一个用于构建用户界面的库...' },
},
],
search_queries: ['markdown parser', 'tree-sitter', 'react components'],
};
return (
<div className="enhanced-markdown-example p-6 max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Markdown渲染器示例</h1>
{/* 控制面板 */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<h2 className="text-lg font-semibold mb-4"></h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={enableMarkdown}
onChange={(e) => setEnableMarkdown(e.target.checked)}
className="rounded"
/>
<span className="text-sm">Markdown</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={showStatistics}
onChange={(e) => setShowStatistics(e.target.checked)}
className="rounded"
/>
<span className="text-sm"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={enableRealTimeParsing}
onChange={(e) => setEnableRealTimeParsing(e.target.checked)}
className="rounded"
/>
<span className="text-sm"></span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={enableReferences}
onChange={(e) => setEnableReferences(e.target.checked)}
className="rounded"
/>
<span className="text-sm"></span>
</label>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 编辑器 */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Markdown编辑器</h2>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full h-96 p-4 border border-gray-300 rounded-lg font-mono text-sm resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="在这里输入Markdown内容..."
/>
<div className="text-sm text-gray-600">
: {content.length} | : {content.split('\n').length}
</div>
</div>
{/* 渲染结果 */}
<div className="space-y-4">
<h2 className="text-xl font-semibold"></h2>
<div className="border border-gray-300 rounded-lg overflow-hidden">
<div className="h-96 overflow-y-auto p-4">
<EnhancedMarkdownRenderer
content={content}
enableMarkdown={enableMarkdown}
showStatistics={showStatistics}
enableRealTimeParsing={enableRealTimeParsing}
enableReferences={enableReferences}
groundingMetadata={enableReferences ? mockGroundingMetadata : undefined}
className="h-full"
/>
</div>
</div>
</div>
</div>
{/* 功能对比 */}
<div className="mt-8 p-6 bg-blue-50 rounded-lg">
<h3 className="text-lg font-semibold mb-4">🔄 ReactMarkdown到MarkdownService的升级</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-red-600 mb-2"> (ReactMarkdown)</h4>
<ul className="text-sm text-gray-600 space-y-1">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div>
<h4 className="font-medium text-green-600 mb-2"> (MarkdownService)</h4>
<ul className="text-sm text-gray-600 space-y-1">
<li> Tree-sitter语法树解析</li>
<li> </li>
<li> </li>
<li> </li>
<li> Rust实现</li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</div>
{/* 使用说明 */}
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-medium text-yellow-800 mb-2">💡 使</h4>
<ul className="text-sm text-yellow-700 space-y-1">
<li> </li>
<li> 使</li>
<li> "显示统计"</li>
<li> "实时解析"</li>
<li> </li>
</ul>
</div>
{/* 技术细节 */}
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<h4 className="font-medium mb-2">🔧 </h4>
<div className="text-sm text-gray-600 space-y-2">
<p><strong>:</strong> Markdown文本 Tauri命令 Rust解析器 pulldown-cmark AST React渲染</p>
<p><strong>:</strong> </p>
<p><strong>:</strong> </p>
<p><strong>:</strong> </p>
</div>
</div>
</div>
);
};
export default EnhancedMarkdownRendererExample;

View File

@@ -1,186 +0,0 @@
import React, { useState } from 'react';
import { EnhancedMarkdownRenderer } from '../components/EnhancedMarkdownRenderer';
import { GroundingMetadata } from '../types/ragGrounding';
/**
* Grounding分析示例
* 展示如何将Markdown节点与引用资源进行关联分析
*/
const GroundingAnalysisExample: React.FC = () => {
const [content] = useState(`# AI技术发展报告
人工智能技术在近年来取得了显著进展。深度学习算法的突破使得机器学习模型在图像识别、自然语言处理等领域达到了前所未有的精度。
## 主要技术突破
### 大语言模型
大语言模型如GPT系列展现了强大的文本生成和理解能力这些模型通过在海量文本数据上进行预训练学习到了丰富的语言知识和常识。
### 计算机视觉
在计算机视觉领域卷积神经网络和Transformer架构的结合产生了新的突破使得图像分类、目标检测等任务的准确率大幅提升。
## 应用前景
AI技术的应用前景广阔从自动驾驶到医疗诊断从智能客服到内容创作人工智能正在改变我们的生活和工作方式。`);
// 模拟包含grounding支持信息的metadata
const mockGroundingMetadata: GroundingMetadata = {
sources: [
{
title: 'AI技术发展白皮书2024',
uri: 'https://ai-research.org/whitepaper-2024',
content: {
snippet: '人工智能技术在深度学习、自然语言处理等领域取得重大突破...',
summary: '详细分析了AI技术的最新发展趋势'
}
},
{
title: 'GPT模型技术解析',
uri: 'https://openai.com/research/gpt-analysis',
content: {
snippet: 'GPT系列模型通过Transformer架构实现了强大的文本生成能力...',
summary: 'GPT模型的技术原理和应用分析'
}
},
{
title: '计算机视觉前沿技术',
uri: 'https://cv-research.com/latest-trends',
content: {
snippet: '卷积神经网络与Transformer的结合为计算机视觉带来新突破...',
summary: '计算机视觉领域的最新技术发展'
}
},
{
title: 'AI应用场景研究报告',
uri: 'https://ai-applications.org/report',
content: {
snippet: 'AI技术在自动驾驶、医疗、客服等领域的应用前景分析...',
summary: 'AI技术的实际应用场景和发展前景'
}
}
],
search_queries: ['AI技术发展', '深度学习', 'GPT模型', '计算机视觉', 'AI应用'],
grounding_supports: [
{
groundingChunkIndices: [0],
segment: {
start_index: 15, // "人工智能技术在近年来取得了显著进展"
end_index: 85
}
},
{
groundingChunkIndices: [1],
segment: {
start_index: 180, // "大语言模型如GPT系列展现了强大的文本生成和理解能力"
end_index: 280
}
},
{
groundingChunkIndices: [2],
segment: {
start_index: 350, // "在计算机视觉领域卷积神经网络和Transformer架构的结合"
end_index: 420
}
},
{
groundingChunkIndices: [3],
segment: {
start_index: 480, // "AI技术的应用前景广阔从自动驾驶到医疗诊断"
end_index: 550
}
}
]
};
return (
<div className="grounding-analysis-example p-6 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Grounding分析示例</h1>
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h2 className="text-lg font-semibold mb-2">🔍 </h2>
<p className="text-sm text-blue-700 mb-2">
Markdown节点与引用资源进行关联分析
</p>
<ul className="text-sm text-blue-700 space-y-1">
<li> 📚 </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
{/* Grounding Metadata 信息展示 */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<h3 className="text-lg font-semibold mb-3">📋 Grounding Metadata </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2"> ({mockGroundingMetadata.sources.length})</h4>
<div className="space-y-2">
{mockGroundingMetadata.sources.map((source, index) => (
<div key={index} className="text-sm p-2 bg-white rounded border">
<div className="font-medium">{source.title}</div>
<div className="text-gray-600 text-xs">{source.uri}</div>
</div>
))}
</div>
</div>
<div>
<h4 className="font-medium mb-2"> ({mockGroundingMetadata.grounding_supports?.length || 0})</h4>
<div className="space-y-2">
{mockGroundingMetadata.grounding_supports?.map((support, index) => (
<div key={index} className="text-sm p-2 bg-white rounded border">
<div>: {support.segment.start_index} - {support.segment.end_index}</div>
<div className="text-gray-600">: [{support.groundingChunkIndices.join(', ')}]</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Markdown渲染 */}
<div className="border border-gray-300 rounded-lg overflow-hidden">
<div className="bg-gray-100 px-4 py-2 border-b">
<h3 className="font-semibold"></h3>
</div>
<div className="p-4">
<EnhancedMarkdownRenderer
content={content}
enableMarkdown={true}
enableReferences={true}
groundingMetadata={mockGroundingMetadata}
showStatistics={true}
/>
</div>
</div>
{/* 分析说明 */}
<div className="mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h3 className="text-lg font-semibold mb-2">🧠 </h3>
<div className="text-sm text-yellow-800 space-y-2">
<p><strong>1. :</strong> Markdown节点的字符偏移位置与grounding片段的位置范围</p>
<p><strong>2. :</strong> grounding支持信息中的索引</p>
<p><strong>3. :</strong> 📚</p>
<p><strong>4. :</strong> </p>
</div>
</div>
{/* 技术实现 */}
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<h3 className="text-lg font-semibold mb-2"> </h3>
<div className="text-sm text-green-800 space-y-2">
<p><strong>:</strong></p>
<ul className="ml-4 space-y-1">
<li> <code>MarkdownNode</code>: </li>
<li> <code>GroundingMetadata</code>: </li>
<li> <code>GroundingSupport</code>: </li>
</ul>
<p><strong>:</strong> </p>
<p><strong>:</strong> 使React组件动态显示引用指示器和详细信息</p>
</div>
</div>
</div>
);
};
export default GroundingAnalysisExample;

View File

@@ -1,202 +0,0 @@
import React, { useState } from 'react';
import MarkdownParserRenderer from '../components/MarkdownParserRenderer';
/**
* Markdown解析器使用示例
* 展示基于Tree-sitter的Markdown解析功能
*/
const MarkdownParserExample: React.FC = () => {
const [markdown, setMarkdown] = useState(`# Tree-sitter Markdown解析器示例
这是一个基于Tree-sitter的Markdown解析器示例展示了原文引用和位置信息保留功能。
## 功能特性
### 1. 精确解析
- 基于Tree-sitter的语法树解析
- 比传统正则表达式更准确
- 支持错误恢复
### 2. 原文引用
- 保留每个节点的精确位置信息
- 行号、列号、字符偏移量
- 支持位置查询和导航
### 3. 多种查询
- **标题查询**: 提取文档大纲
- **链接查询**: 查找所有链接和图片
- **代码查询**: 查找代码块和内联代码
## 示例内容
### 链接和图片
- [Google](https://google.com "Google搜索")
- [GitHub](https://github.com)
- ![示例图片](https://example.com/image.png "示例图片")
### 代码示例
\`\`\`typescript
// TypeScript代码示例
interface MarkdownNode {
node_type: MarkdownNodeType;
content: string;
range: Range;
children: MarkdownNode[];
attributes: Record<string, string>;
}
function parseMarkdown(text: string): MarkdownParseResult {
// 解析逻辑
return result;
}
\`\`\`
\`\`\`rust
// Rust代码示例
use pulldown_cmark::{Parser, Event, Tag};
pub fn parse_markdown(text: &str) -> Result<MarkdownParseResult> {
let parser = Parser::new(text);
// 解析逻辑
Ok(result)
}
\`\`\`
### 列表示例
#### 无序列表
- 项目1
- 项目2
- 子项目2.1
- 子项目2.2
- 项目3
#### 有序列表
1. 第一项
2. 第二项
3. 第三项
### 引用和强调
> 这是一个引用块。
>
> 它可以包含**粗体**和*斜体*文本。
### 表格
| 功能 | 描述 | 状态 |
|------|------|------|
| 解析 | Markdown解析 | ✅ |
| 查询 | 节点查询 | ✅ |
| 验证 | 文档验证 | ✅ |
| 位置 | 位置信息 | ✅ |
---
## 技术实现
这个解析器使用了以下技术:
1. **后端**: Rust + pulldown-cmark
2. **前端**: React + TypeScript
3. **通信**: Tauri命令系统
4. **位置跟踪**: 自定义位置计算算法
### 内联代码
使用 \`markdownService.parseMarkdown()\` 方法进行解析。
## 总结
这个基于Tree-sitter的Markdown解析器提供了
- 精确的语法解析
- 完整的位置信息
- 灵活的查询接口
- 实时解析支持
`);
const handleNodeClick = (node: any) => {
console.log('点击的节点:', node);
alert(`节点类型: ${node.node_type}\n位置: ${node.range.start.line + 1}:${node.range.start.column + 1}\n内容长度: ${node.content.length} 字符`);
};
const handleParseComplete = (result: any) => {
console.log('解析完成:', result);
};
return (
<div className="markdown-parser-example p-6 max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Tree-sitter Markdown解析器示例</h1>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 编辑器 */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Markdown编辑器</h2>
<textarea
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
className="w-full h-96 p-4 border border-gray-300 rounded-lg font-mono text-sm resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="在这里输入Markdown内容..."
/>
<div className="text-sm text-gray-600">
: {markdown.length} | : {markdown.split('\n').length}
</div>
</div>
{/* 解析结果 */}
<div className="space-y-4">
<h2 className="text-xl font-semibold"></h2>
<div className="border border-gray-300 rounded-lg overflow-hidden">
<MarkdownParserRenderer
content={markdown}
showOutline={true}
showLinks={true}
showValidation={true}
showPositionInfo={true}
enableRealTimeParsing={true}
className="h-96 overflow-y-auto"
onNodeClick={handleNodeClick}
onParseComplete={handleParseComplete}
/>
</div>
</div>
</div>
{/* 功能说明 */}
<div className="mt-8 p-6 bg-gray-50 rounded-lg">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded border">
<h4 className="font-medium text-blue-600 mb-2">🎯 </h4>
<p className="text-sm text-gray-600">pulldown-cmark的语法树解析</p>
</div>
<div className="bg-white p-4 rounded border">
<h4 className="font-medium text-green-600 mb-2">📍 </h4>
<p className="text-sm text-gray-600"></p>
</div>
<div className="bg-white p-4 rounded border">
<h4 className="font-medium text-purple-600 mb-2">🔍 </h4>
<p className="text-sm text-gray-600"></p>
</div>
<div className="bg-white p-4 rounded border">
<h4 className="font-medium text-orange-600 mb-2"> </h4>
<p className="text-sm text-gray-600"></p>
</div>
</div>
</div>
{/* 使用提示 */}
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-800 mb-2">💡 使</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
);
};
export default MarkdownParserExample;

View File

@@ -1,347 +0,0 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { EnhancedMarkdownRenderer } from '../../components/EnhancedMarkdownRenderer';
import { markdownService } from '../../services/markdownService';
import {
MarkdownParseResult,
MarkdownNode,
MarkdownNodeType,
ValidationResult,
} from '../../types/markdown';
// Mock the markdown service
vi.mock('../../services/markdownService', () => ({
markdownService: {
parseMarkdown: vi.fn(),
validateMarkdown: vi.fn(),
extractTextContent: vi.fn(),
},
}));
const mockMarkdownService = markdownService as any;
describe('EnhancedMarkdownRenderer', () => {
const mockParseResult: MarkdownParseResult = {
root: {
node_type: MarkdownNodeType.Document,
content: '# Test\n\nHello **world**',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 2, column: 12, offset: 19 },
},
children: [
{
node_type: MarkdownNodeType.Heading,
content: '# Test',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 6, offset: 6 },
},
children: [
{
node_type: MarkdownNodeType.Text,
content: 'Test',
range: {
start: { line: 0, column: 2, offset: 2 },
end: { line: 0, column: 6, offset: 6 },
},
children: [],
attributes: {},
},
],
attributes: { level: '1' },
},
{
node_type: MarkdownNodeType.Paragraph,
content: 'Hello **world**',
range: {
start: { line: 2, column: 0, offset: 8 },
end: { line: 2, column: 15, offset: 23 },
},
children: [
{
node_type: MarkdownNodeType.Text,
content: 'Hello ',
range: {
start: { line: 2, column: 0, offset: 8 },
end: { line: 2, column: 6, offset: 14 },
},
children: [],
attributes: {},
},
{
node_type: MarkdownNodeType.Strong,
content: '**world**',
range: {
start: { line: 2, column: 6, offset: 14 },
end: { line: 2, column: 15, offset: 23 },
},
children: [
{
node_type: MarkdownNodeType.Text,
content: 'world',
range: {
start: { line: 2, column: 8, offset: 16 },
end: { line: 2, column: 13, offset: 21 },
},
children: [],
attributes: {},
},
],
attributes: {},
},
],
attributes: {},
},
],
attributes: {},
},
statistics: {
total_nodes: 5,
error_nodes: 0,
parse_time_ms: 3,
document_length: 19,
max_depth: 3,
},
source_text: '# Test\n\nHello **world**',
};
const mockValidation: ValidationResult = {
is_valid: true,
issues: [],
statistics: {
total_nodes: 5,
error_nodes: 0,
parse_time_ms: 3,
document_length: 19,
max_depth: 3,
},
};
beforeEach(() => {
mockMarkdownService.parseMarkdown.mockResolvedValue(mockParseResult);
mockMarkdownService.validateMarkdown.mockResolvedValue(mockValidation);
mockMarkdownService.extractTextContent.mockImplementation((node: MarkdownNode) => {
if (node.node_type === MarkdownNodeType.Text) {
return node.content;
}
return node.children.map(child => mockMarkdownService.extractTextContent(child)).join('');
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders markdown content using MarkdownService', async () => {
render(
<EnhancedMarkdownRenderer
content="# Test\n\nHello **world**"
enableMarkdown={true}
/>
);
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith(expect.stringContaining('# Test'));
});
await waitFor(() => {
expect(screen.getByText('Test')).toBeInTheDocument();
expect(screen.getByText('Hello')).toBeInTheDocument();
expect(screen.getByText('world')).toBeInTheDocument();
});
});
it('renders plain text when markdown is disabled', () => {
render(
<EnhancedMarkdownRenderer
content="# Test\n\nHello **world**"
enableMarkdown={false}
/>
);
expect(screen.getByText(/# Test.*Hello \*\*world\*\*/s)).toBeInTheDocument();
expect(mockMarkdownService.parseMarkdown).not.toHaveBeenCalled();
});
it('shows loading state during parsing', () => {
// Make parseMarkdown return a pending promise
mockMarkdownService.parseMarkdown.mockReturnValue(new Promise(() => {}));
render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
/>
);
expect(screen.getByText('解析中...')).toBeInTheDocument();
});
it('shows error state when parsing fails', async () => {
mockMarkdownService.parseMarkdown.mockRejectedValue(new Error('Parse error'));
render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
/>
);
await waitFor(() => {
expect(screen.getByText('解析错误')).toBeInTheDocument();
expect(screen.getByText('Parse error')).toBeInTheDocument();
});
});
it('shows statistics when enabled', async () => {
render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
showStatistics={true}
/>
);
await waitFor(() => {
expect(screen.getByText('解析统计')).toBeInTheDocument();
expect(screen.getByText('节点数: 5')).toBeInTheDocument();
expect(screen.getByText('解析时间: 3ms')).toBeInTheDocument();
});
});
it('shows validation warnings when document is invalid', async () => {
const invalidValidation: ValidationResult = {
is_valid: false,
issues: [
{
issue_type: 'SkippedHeadingLevel' as any,
message: 'Heading level jumps from 1 to 3',
range: {
start: { line: 2, column: 0, offset: 10 },
end: { line: 2, column: 10, offset: 20 },
},
severity: 'Warning' as any,
},
],
statistics: mockValidation.statistics,
};
mockMarkdownService.validateMarkdown.mockResolvedValue(invalidValidation);
render(
<EnhancedMarkdownRenderer
content="# Test\n\n### Skipped"
enableMarkdown={true}
/>
);
await waitFor(() => {
expect(screen.getByText('文档验证警告')).toBeInTheDocument();
expect(screen.getByText('• Heading level jumps from 1 to 3')).toBeInTheDocument();
});
});
it('renders grounding metadata when provided', async () => {
const groundingMetadata = {
sources: [
{ title: 'Source 1', url: 'https://example.com/1' },
{ title: 'Source 2', url: 'https://example.com/2' },
],
};
render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
enableReferences={true}
groundingMetadata={groundingMetadata}
/>
);
await waitFor(() => {
expect(screen.getByText('基于 2 个来源的信息')).toBeInTheDocument();
expect(screen.getByText('1')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
});
});
it('handles real-time parsing when enabled', async () => {
const { rerender } = render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
enableRealTimeParsing={true}
/>
);
// Change content
rerender(
<EnhancedMarkdownRenderer
content="# Updated Test"
enableMarkdown={true}
enableRealTimeParsing={true}
/>
);
// Wait for debounced parsing
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith('# Updated Test');
}, { timeout: 1000 });
});
it('applies custom className', () => {
const { container } = render(
<EnhancedMarkdownRenderer
content="# Test"
enableMarkdown={true}
className="custom-class"
/>
);
expect(container.firstChild).toHaveClass('custom-class');
});
it('renders different heading levels correctly', async () => {
const headingResult: MarkdownParseResult = {
...mockParseResult,
root: {
...mockParseResult.root,
children: [
{
node_type: MarkdownNodeType.Heading,
content: '# H1',
range: { start: { line: 0, column: 0, offset: 0 }, end: { line: 0, column: 4, offset: 4 } },
children: [{ node_type: MarkdownNodeType.Text, content: 'H1', range: { start: { line: 0, column: 2, offset: 2 }, end: { line: 0, column: 4, offset: 4 } }, children: [], attributes: {} }],
attributes: { level: '1' },
},
{
node_type: MarkdownNodeType.Heading,
content: '## H2',
range: { start: { line: 1, column: 0, offset: 5 }, end: { line: 1, column: 5, offset: 10 } },
children: [{ node_type: MarkdownNodeType.Text, content: 'H2', range: { start: { line: 1, column: 3, offset: 8 }, end: { line: 1, column: 5, offset: 10 } }, children: [], attributes: {} }],
attributes: { level: '2' },
},
],
},
};
mockMarkdownService.parseMarkdown.mockResolvedValue(headingResult);
render(
<EnhancedMarkdownRenderer
content="# H1\n## H2"
enableMarkdown={true}
/>
);
await waitFor(() => {
const h1 = screen.getByRole('heading', { level: 1 });
const h2 = screen.getByRole('heading', { level: 2 });
expect(h1.textContent).toBe('H1');
expect(h2.textContent).toBe('H2');
});
});
});

View File

@@ -1,395 +0,0 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import MarkdownParserRenderer from '../../components/MarkdownParserRenderer';
import { markdownService } from '../../services/markdownService';
import {
MarkdownParseResult,
MarkdownNode,
MarkdownNodeType,
OutlineItem,
LinkInfo,
ValidationResult,
ValidationSeverity,
ValidationIssueType,
LinkType,
} from '../../types/markdown';
// Mock the markdown service
vi.mock('../../services/markdownService', () => ({
markdownService: {
parseMarkdown: vi.fn(),
extractOutline: vi.fn(),
extractLinks: vi.fn(),
validateMarkdown: vi.fn(),
extractTextContent: vi.fn(),
formatRange: vi.fn(),
},
}));
const mockMarkdownService = markdownService as any;
describe('MarkdownParserRenderer', () => {
const mockParseResult: MarkdownParseResult = {
root: {
node_type: MarkdownNodeType.Document,
content: '# Test\n\nHello world',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 2, column: 11, offset: 18 },
},
children: [
{
node_type: MarkdownNodeType.Heading,
content: '# Test',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 6, offset: 6 },
},
children: [],
attributes: { level: '1' },
},
{
node_type: MarkdownNodeType.Paragraph,
content: 'Hello world',
range: {
start: { line: 2, column: 0, offset: 8 },
end: { line: 2, column: 11, offset: 18 },
},
children: [],
attributes: {},
},
],
attributes: {},
},
statistics: {
total_nodes: 3,
error_nodes: 0,
parse_time_ms: 5,
document_length: 18,
max_depth: 2,
},
source_text: '# Test\n\nHello world',
};
const mockOutline: OutlineItem[] = [
{
title: 'Test',
level: 1,
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 6, offset: 6 },
},
},
];
const mockLinks: LinkInfo[] = [
{
text: 'Google',
url: 'https://google.com',
title: undefined,
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 20, offset: 20 },
},
link_type: LinkType.Link,
},
];
const mockValidation: ValidationResult = {
is_valid: true,
issues: [],
statistics: {
total_nodes: 3,
error_nodes: 0,
parse_time_ms: 5,
document_length: 18,
max_depth: 2,
},
};
beforeEach(() => {
mockMarkdownService.parseMarkdown.mockResolvedValue(mockParseResult);
mockMarkdownService.extractOutline.mockResolvedValue(mockOutline);
mockMarkdownService.extractLinks.mockResolvedValue(mockLinks);
mockMarkdownService.validateMarkdown.mockResolvedValue(mockValidation);
mockMarkdownService.extractTextContent.mockImplementation((node: MarkdownNode) => {
if (node.node_type === MarkdownNodeType.Text) {
return node.content;
}
return node.children.map(child => mockMarkdownService.extractTextContent(child)).join('');
});
mockMarkdownService.formatRange.mockImplementation((start: any, end: any) => {
return `${start.line + 1}:${start.column + 1}-${end.line + 1}:${end.column + 1}`;
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders loading state initially', () => {
render(<MarkdownParserRenderer content="# Test" />);
expect(screen.getByText('解析中...')).toBeInTheDocument();
});
it('renders parsed markdown content', async () => {
render(<MarkdownParserRenderer content="# Test\n\nHello world" />);
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith(
'# Test\n\nHello world',
undefined
);
});
await waitFor(() => {
expect(screen.queryByText('解析中...')).not.toBeInTheDocument();
});
});
it('renders outline when showOutline is true', async () => {
render(
<MarkdownParserRenderer
content="# Test\n\nHello world"
showOutline={true}
/>
);
await waitFor(() => {
expect(mockMarkdownService.extractOutline).toHaveBeenCalledWith('# Test\n\nHello world');
});
await waitFor(() => {
expect(screen.getByText('目录')).toBeInTheDocument();
});
});
it('renders links when showLinks is true', async () => {
render(
<MarkdownParserRenderer
content="[Google](https://google.com)"
showLinks={true}
/>
);
await waitFor(() => {
expect(mockMarkdownService.extractLinks).toHaveBeenCalledWith('[Google](https://google.com)');
});
await waitFor(() => {
expect(screen.getByText('链接')).toBeInTheDocument();
});
});
it('renders validation results when showValidation is true', async () => {
render(
<MarkdownParserRenderer
content="# Test"
showValidation={true}
/>
);
await waitFor(() => {
expect(mockMarkdownService.validateMarkdown).toHaveBeenCalledWith('# Test');
});
await waitFor(() => {
expect(screen.getByText('验证结果 ✅')).toBeInTheDocument();
});
});
it('renders validation issues when document is invalid', async () => {
const invalidValidation: ValidationResult = {
is_valid: false,
issues: [
{
issue_type: ValidationIssueType.SkippedHeadingLevel,
message: 'Heading level jumps from 1 to 3',
range: {
start: { line: 2, column: 0, offset: 10 },
end: { line: 2, column: 10, offset: 20 },
},
severity: ValidationSeverity.Warning,
},
],
statistics: mockValidation.statistics,
};
mockMarkdownService.validateMarkdown.mockResolvedValue(invalidValidation);
render(
<MarkdownParserRenderer
content="# Test\n\n### Skipped"
showValidation={true}
/>
);
await waitFor(() => {
expect(screen.getByText('验证结果 ❌')).toBeInTheDocument();
expect(screen.getByText('Heading level jumps from 1 to 3')).toBeInTheDocument();
});
});
it('handles parsing errors gracefully', async () => {
mockMarkdownService.parseMarkdown.mockRejectedValue(new Error('Parse error'));
render(<MarkdownParserRenderer content="# Test" />);
await waitFor(() => {
expect(screen.getByText('解析错误')).toBeInTheDocument();
expect(screen.getByText('Parse error')).toBeInTheDocument();
});
});
it('calls onParseComplete when parsing is successful', async () => {
const onParseComplete = vi.fn();
render(
<MarkdownParserRenderer
content="# Test"
onParseComplete={onParseComplete}
/>
);
await waitFor(() => {
expect(onParseComplete).toHaveBeenCalledWith(mockParseResult);
});
});
it('calls onNodeClick when a node is clicked', async () => {
const onNodeClick = vi.fn();
render(
<MarkdownParserRenderer
content="# Test"
onNodeClick={onNodeClick}
/>
);
await waitFor(() => {
expect(screen.queryByText('解析中...')).not.toBeInTheDocument();
});
// Find and click a node (this is a simplified test)
const nodes = screen.getAllByRole('button', { hidden: true });
if (nodes.length > 0) {
fireEvent.click(nodes[0]);
expect(onNodeClick).toHaveBeenCalled();
}
});
it('shows position info when showPositionInfo is true', async () => {
render(
<MarkdownParserRenderer
content="# Test"
showPositionInfo={true}
/>
);
await waitFor(() => {
expect(screen.queryByText('解析中...')).not.toBeInTheDocument();
});
// Check if position info is displayed
expect(mockMarkdownService.formatRange).toHaveBeenCalled();
});
it('handles empty content', async () => {
render(<MarkdownParserRenderer content="" />);
await waitFor(() => {
expect(screen.getByText('暂无内容')).toBeInTheDocument();
});
});
it('enables real-time parsing when enableRealTimeParsing is true', async () => {
const { rerender } = render(
<MarkdownParserRenderer
content="# Test"
enableRealTimeParsing={true}
/>
);
// Change content
rerender(
<MarkdownParserRenderer
content="# Updated Test"
enableRealTimeParsing={true}
/>
);
// Wait for debounced parsing
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith(
'# Updated Test',
undefined
);
}, { timeout: 1000 });
});
it('applies custom className', () => {
const { container } = render(
<MarkdownParserRenderer
content="# Test"
className="custom-class"
/>
);
expect(container.firstChild).toHaveClass('custom-class');
});
it('uses custom parser config', async () => {
const config = {
preserve_whitespace: true,
parse_inline_html: false,
max_depth: 50,
timeout_ms: 5000,
};
render(
<MarkdownParserRenderer
content="# Test"
config={config}
/>
);
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledWith(
'# Test',
config
);
});
});
it('displays parse statistics', async () => {
render(<MarkdownParserRenderer content="# Test" />);
await waitFor(() => {
expect(screen.getByText('解析统计')).toBeInTheDocument();
expect(screen.getByText('总节点数: 3')).toBeInTheDocument();
expect(screen.getByText('错误节点数: 0')).toBeInTheDocument();
expect(screen.getByText('解析耗时: 5ms')).toBeInTheDocument();
expect(screen.getByText('最大深度: 2')).toBeInTheDocument();
});
});
it('retries parsing when retry button is clicked', async () => {
mockMarkdownService.parseMarkdown.mockRejectedValueOnce(new Error('Parse error'));
render(<MarkdownParserRenderer content="# Test" />);
await waitFor(() => {
expect(screen.getByText('解析错误')).toBeInTheDocument();
});
// Reset mock to succeed on retry
mockMarkdownService.parseMarkdown.mockResolvedValue(mockParseResult);
const retryButton = screen.getByText('重试');
fireEvent.click(retryButton);
await waitFor(() => {
expect(mockMarkdownService.parseMarkdown).toHaveBeenCalledTimes(2);
});
});
});

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { describe, it, expect, beforeAll } from 'vitest';
import { markdownService } from '../../services/markdownService';
import {
MarkdownNodeType,

View File

@@ -1,436 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import { markdownService } from '../../services/markdownService';
import {
MarkdownParseResult,
MarkdownNode,
MarkdownNodeType,
OutlineItem,
LinkInfo,
ValidationResult,
LinkType,
QueryType,
} from '../../types/markdown';
// Mock Tauri invoke
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
const mockInvoke = invoke as any;
describe('MarkdownService', () => {
const mockParseResult: MarkdownParseResult = {
root: {
node_type: MarkdownNodeType.Document,
content: '# Test\n\nHello world',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 2, column: 11, offset: 18 },
},
children: [
{
node_type: MarkdownNodeType.Heading,
content: '# Test',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 6, offset: 6 },
},
children: [
{
node_type: MarkdownNodeType.Text,
content: 'Test',
range: {
start: { line: 0, column: 2, offset: 2 },
end: { line: 0, column: 6, offset: 6 },
},
children: [],
attributes: {},
},
],
attributes: { level: '1' },
},
],
attributes: {},
},
statistics: {
total_nodes: 3,
error_nodes: 0,
parse_time_ms: 5,
document_length: 18,
max_depth: 2,
},
source_text: '# Test\n\nHello world',
};
const mockOutline: OutlineItem[] = [
{
title: 'Test',
level: 1,
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 6, offset: 6 },
},
},
];
const mockLinks: LinkInfo[] = [
{
text: 'Google',
url: 'https://google.com',
title: undefined,
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 20, offset: 20 },
},
link_type: LinkType.Link,
},
];
const mockValidation: ValidationResult = {
is_valid: true,
issues: [],
statistics: {
total_nodes: 3,
error_nodes: 0,
parse_time_ms: 5,
document_length: 18,
max_depth: 2,
},
};
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('parseMarkdown', () => {
it('should parse markdown successfully', async () => {
mockInvoke.mockResolvedValue({
success: true,
result: mockParseResult,
});
const result = await markdownService.parseMarkdown('# Test\n\nHello world');
expect(mockInvoke).toHaveBeenCalledWith('parse_markdown', {
request: {
text: '# Test\n\nHello world',
config: undefined,
},
});
expect(result).toEqual(mockParseResult);
});
it('should parse markdown with config', async () => {
const config = {
preserve_whitespace: true,
parse_inline_html: false,
};
mockInvoke.mockResolvedValue({
success: true,
result: mockParseResult,
});
await markdownService.parseMarkdown('# Test', config);
expect(mockInvoke).toHaveBeenCalledWith('parse_markdown', {
request: {
text: '# Test',
config,
},
});
});
it('should throw error when parsing fails', async () => {
mockInvoke.mockResolvedValue({
success: false,
error: 'Parse error',
});
await expect(markdownService.parseMarkdown('# Test')).rejects.toThrow('Parse error');
});
it('should throw error when result is missing', async () => {
mockInvoke.mockResolvedValue({
success: true,
result: null,
});
await expect(markdownService.parseMarkdown('# Test')).rejects.toThrow('Failed to parse markdown');
});
});
describe('queryNodes', () => {
it('should query nodes successfully', async () => {
const mockNodes: MarkdownNode[] = [mockParseResult.root.children[0]];
mockInvoke.mockResolvedValue({
success: true,
nodes: mockNodes,
});
const result = await markdownService.queryNodes('# Test', QueryType.Headings);
expect(mockInvoke).toHaveBeenCalledWith('query_markdown_nodes', {
request: {
text: '# Test',
query_name: QueryType.Headings,
},
});
expect(result).toEqual(mockNodes);
});
it('should throw error when query fails', async () => {
mockInvoke.mockResolvedValue({
success: false,
error: 'Query error',
});
await expect(markdownService.queryNodes('# Test', QueryType.Headings)).rejects.toThrow('Query error');
});
});
describe('findNodeAtPosition', () => {
it('should find node at position successfully', async () => {
const mockNode = mockParseResult.root.children[0];
mockInvoke.mockResolvedValue({
success: true,
node: mockNode,
});
const result = await markdownService.findNodeAtPosition('# Test', 0, 0);
expect(mockInvoke).toHaveBeenCalledWith('find_markdown_node_at_position', {
request: {
text: '# Test',
line: 0,
column: 0,
},
});
expect(result).toEqual(mockNode);
});
it('should return null when no node found', async () => {
mockInvoke.mockResolvedValue({
success: true,
node: null,
});
const result = await markdownService.findNodeAtPosition('# Test', 10, 10);
expect(result).toBeNull();
});
it('should throw error when find fails', async () => {
mockInvoke.mockResolvedValue({
success: false,
error: 'Find error',
});
await expect(markdownService.findNodeAtPosition('# Test', 0, 0)).rejects.toThrow('Find error');
});
});
describe('extractOutline', () => {
it('should extract outline successfully', async () => {
mockInvoke.mockResolvedValue({
success: true,
outline: mockOutline,
});
const result = await markdownService.extractOutline('# Test');
expect(mockInvoke).toHaveBeenCalledWith('extract_markdown_outline', {
request: {
text: '# Test',
},
});
expect(result).toEqual(mockOutline);
});
it('should throw error when extraction fails', async () => {
mockInvoke.mockResolvedValue({
success: false,
error: 'Extraction error',
});
await expect(markdownService.extractOutline('# Test')).rejects.toThrow('Extraction error');
});
});
describe('extractLinks', () => {
it('should extract links successfully', async () => {
mockInvoke.mockResolvedValue({
success: true,
links: mockLinks,
});
const result = await markdownService.extractLinks('[Google](https://google.com)');
expect(mockInvoke).toHaveBeenCalledWith('extract_markdown_links', {
request: {
text: '[Google](https://google.com)',
},
});
expect(result).toEqual(mockLinks);
});
it('should throw error when extraction fails', async () => {
mockInvoke.mockResolvedValue({
success: false,
error: 'Extraction error',
});
await expect(markdownService.extractLinks('[Google](https://google.com)')).rejects.toThrow('Extraction error');
});
});
describe('validateMarkdown', () => {
it('should validate markdown successfully', async () => {
mockInvoke.mockResolvedValue({
success: true,
validation: mockValidation,
});
const result = await markdownService.validateMarkdown('# Test');
expect(mockInvoke).toHaveBeenCalledWith('validate_markdown', {
request: {
text: '# Test',
},
});
expect(result).toEqual(mockValidation);
});
it('should throw error when validation fails', async () => {
mockInvoke.mockResolvedValue({
success: false,
error: 'Validation error',
});
await expect(markdownService.validateMarkdown('# Test')).rejects.toThrow('Validation error');
});
});
describe('convenience methods', () => {
beforeEach(() => {
mockInvoke.mockResolvedValue({
success: true,
nodes: [mockParseResult.root.children[0]],
});
});
it('should query headings', async () => {
await markdownService.queryHeadings('# Test');
expect(mockInvoke).toHaveBeenCalledWith('query_markdown_nodes', {
request: {
text: '# Test',
query_name: QueryType.Headings,
},
});
});
it('should query link nodes', async () => {
await markdownService.queryLinkNodes('[Link](url)');
expect(mockInvoke).toHaveBeenCalledWith('query_markdown_nodes', {
request: {
text: '[Link](url)',
query_name: QueryType.Links,
},
});
});
it('should query code blocks', async () => {
await markdownService.queryCodeBlocks('```js\ncode\n```');
expect(mockInvoke).toHaveBeenCalledWith('query_markdown_nodes', {
request: {
text: '```js\ncode\n```',
query_name: QueryType.Code,
},
});
});
});
describe('utility methods', () => {
const textNode: MarkdownNode = {
node_type: MarkdownNodeType.Text,
content: 'Hello world',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 11, offset: 11 },
},
children: [],
attributes: {},
};
const headingNode: MarkdownNode = {
node_type: MarkdownNodeType.Heading,
content: '# Test',
range: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 6, offset: 6 },
},
children: [textNode],
attributes: { level: '1' },
};
it('should extract text content from text node', () => {
const result = markdownService.extractTextContent(textNode);
expect(result).toBe('Hello world');
});
it('should extract text content from node with children', () => {
const result = markdownService.extractTextContent(headingNode);
expect(result).toBe('Hello world');
});
it('should check if node is in range', () => {
const result = markdownService.isNodeInRange(textNode, 0, 1);
expect(result).toBe(true);
const result2 = markdownService.isNodeInRange(textNode, 1, 2);
expect(result2).toBe(false);
});
it('should get node line count', () => {
const result = markdownService.getNodeLineCount(textNode);
expect(result).toBe(1);
});
it('should find nodes containing text', () => {
const nodes = [textNode, headingNode];
const result = markdownService.findNodesContainingText(nodes, 'Hello');
expect(result).toHaveLength(2); // Both nodes contain "Hello"
});
it('should get node depth', () => {
const result = markdownService.getNodeDepth(headingNode);
expect(result).toBe(1); // Has one level of children
});
it('should format position', () => {
const result = markdownService.formatPosition(0, 0);
expect(result).toBe('1:1'); // 1-based indexing
});
it('should format range', () => {
const start = { line: 0, column: 0 };
const end = { line: 1, column: 5 };
const result = markdownService.formatRange(start, end);
expect(result).toBe('1:1-2:6'); // 1-based indexing
});
});
describe('singleton pattern', () => {
it('should return the same instance', () => {
const instance1 = markdownService;
const instance2 = markdownService;
expect(instance1).toBe(instance2);
});
});
});