fix: system 系统提示词优化
This commit is contained in:
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* 聊天素材卡片属性接口
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 "这是标题")
|
||||
- 
|
||||
|
||||
### 列表
|
||||
|
||||
#### 无序列表
|
||||
- 项目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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
- 
|
||||
|
||||
### 代码示例
|
||||
|
||||
\`\`\`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;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user