## Phase 2 - 核心功能 (P0) - Issue #5: EPUB/MOBI/AZW3 格式支持 ✅ - 修复 mobi 库 API 调用 (content_raw → content_as_string) - 修复 title()/author() 返回类型 - 添加元数据提取功能 - Issue #6: Markdown 阅读模式 ✅ - 实现 parse_markdown_with_metadata - 支持 Front Matter (YAML) 解析 - 使用 pulldown-cmark 解析引擎 - 支持代码文件高亮 - Issue #7: 双语翻译功能 ✅ - 实现 TranslationService (阿里百炼/DeepL/Ollama) - 语言自动检测 - 双语对照 HTML 渲染 (并排/段落交错模式) - Issue #8: 笔记与书签系统 ✅ - BookmarkManager (高亮/下划线/波浪线/边注) - NoteManager (阅读笔记/想法/问题/总结) - 阅读统计 (时长/会话数/笔记数) - 导出 Markdown/CSV/Anki ## Phase 3 - 高级功能 (P1) - Issue #9: 代码阅读器 ✅ - 支持 20+ 编程语言 - syntect 语法高亮 - 行号显示/代码折叠 - Issue #10: 全文双语对照 ✅ - 段落级翻译对照 - 并排/交错两种模式 - 响应式布局 - Issue #11: 阅读进度同步 ✅ - 本地进度追踪 - 云端同步支持 - 多设备冲突解决 - Issue #12: 插件系统 ✅ - 插件加载/卸载/启用/禁用 - 插件依赖管理 - 内置主题/快捷键插件 ## Phase 4 - 性能与生态 (P1) - Issue #13: 性能优化 ✅ - PerformanceProfiler 性能分析 - CacheManager LRU 缓存 - 性能监控与优化建议 ## 技术栈更新 - 新增依赖:reqwest, uuid, chrono(serde) - 核心模块:8 个 (document/translation/bookmark/note/code_reader/progress/plugin/performance) - 代码量:~5000 行 --- 🚀 ReadFlow MVP 核心功能全部完成!
429 lines
12 KiB
Rust
429 lines
12 KiB
Rust
//! 代码阅读器模块
|
|
//!
|
|
//! 支持语法高亮、代码折叠、行号显示等功能
|
|
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use syntect::easy::HighlightLines;
|
|
use syntect::highlighting::{ThemeSet, Style};
|
|
use syntect::parsing::SyntaxSet;
|
|
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
|
|
|
|
/// 代码语言
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum CodeLanguage {
|
|
Rust,
|
|
JavaScript,
|
|
TypeScript,
|
|
Python,
|
|
Go,
|
|
Java,
|
|
C,
|
|
Cpp,
|
|
CSharp,
|
|
Ruby,
|
|
Swift,
|
|
Kotlin,
|
|
Scala,
|
|
Shell,
|
|
Sql,
|
|
Html,
|
|
Css,
|
|
Json,
|
|
Yaml,
|
|
Xml,
|
|
Markdown,
|
|
Unknown,
|
|
}
|
|
|
|
impl CodeLanguage {
|
|
/// 从文件扩展名判断语言
|
|
pub fn from_extension(ext: &str) -> Self {
|
|
match ext.to_lowercase().as_str() {
|
|
"rs" => CodeLanguage::Rust,
|
|
"js" | "mjs" | "cjs" => CodeLanguage::JavaScript,
|
|
"ts" | "tsx" => CodeLanguage::TypeScript,
|
|
"py" | "pyw" => CodeLanguage::Python,
|
|
"go" => CodeLanguage::Go,
|
|
"java" => CodeLanguage::Java,
|
|
"c" | "h" => CodeLanguage::C,
|
|
"cpp" | "cc" | "cxx" | "hpp" => CodeLanguage::Cpp,
|
|
"cs" => CodeLanguage::CSharp,
|
|
"rb" => CodeLanguage::Ruby,
|
|
"swift" => CodeLanguage::Swift,
|
|
"kt" | "kts" => CodeLanguage::Kotlin,
|
|
"scala" | "sc" => CodeLanguage::Scala,
|
|
"sh" | "bash" | "zsh" | "fish" => CodeLanguage::Shell,
|
|
"sql" => CodeLanguage::Sql,
|
|
"html" | "htm" => CodeLanguage::Html,
|
|
"css" | "scss" | "sass" | "less" => CodeLanguage::Css,
|
|
"json" => CodeLanguage::Json,
|
|
"yaml" | "yml" => CodeLanguage::Yaml,
|
|
"xml" | "svg" => CodeLanguage::Xml,
|
|
"md" | "markdown" => CodeLanguage::Markdown,
|
|
_ => CodeLanguage::Unknown,
|
|
}
|
|
}
|
|
|
|
/// 获取 syntect 语法名称
|
|
pub fn to_syntect_name(&self) -> &'static str {
|
|
match self {
|
|
CodeLanguage::Rust => "Rust",
|
|
CodeLanguage::JavaScript => "JavaScript",
|
|
CodeLanguage::TypeScript => "TypeScript",
|
|
CodeLanguage::Python => "Python",
|
|
CodeLanguage::Go => "Go",
|
|
CodeLanguage::Java => "Java",
|
|
CodeLanguage::C => "C",
|
|
CodeLanguage::Cpp => "C++",
|
|
CodeLanguage::CSharp => "C#",
|
|
CodeLanguage::Ruby => "Ruby",
|
|
CodeLanguage::Swift => "Swift",
|
|
CodeLanguage::Kotlin => "Kotlin",
|
|
CodeLanguage::Scala => "Scala",
|
|
CodeLanguage::Shell => "Bash (shell)",
|
|
CodeLanguage::Sql => "SQL",
|
|
CodeLanguage::Html => "HTML",
|
|
CodeLanguage::Css => "CSS",
|
|
CodeLanguage::Json => "JSON",
|
|
CodeLanguage::Yaml => "YAML",
|
|
CodeLanguage::Xml => "XML",
|
|
CodeLanguage::Markdown => "Markdown",
|
|
CodeLanguage::Unknown => "Plain Text",
|
|
}
|
|
}
|
|
|
|
/// 获取语言显示名称
|
|
pub fn display_name(&self) -> &'static str {
|
|
match self {
|
|
CodeLanguage::Rust => "Rust",
|
|
CodeLanguage::JavaScript => "JavaScript",
|
|
CodeLanguage::TypeScript => "TypeScript",
|
|
CodeLanguage::Python => "Python",
|
|
CodeLanguage::Go => "Go",
|
|
CodeLanguage::Java => "Java",
|
|
CodeLanguage::C => "C",
|
|
CodeLanguage::Cpp => "C++",
|
|
CodeLanguage::CSharp => "C#",
|
|
CodeLanguage::Ruby => "Ruby",
|
|
CodeLanguage::Swift => "Swift",
|
|
CodeLanguage::Kotlin => "Kotlin",
|
|
CodeLanguage::Scala => "Scala",
|
|
CodeLanguage::Shell => "Shell",
|
|
CodeLanguage::Sql => "SQL",
|
|
CodeLanguage::Html => "HTML",
|
|
CodeLanguage::Css => "CSS",
|
|
CodeLanguage::Json => "JSON",
|
|
CodeLanguage::Yaml => "YAML",
|
|
CodeLanguage::Xml => "XML",
|
|
CodeLanguage::Markdown => "Markdown",
|
|
CodeLanguage::Unknown => "Unknown",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 代码行
|
|
#[derive(Debug, Clone)]
|
|
pub struct CodeLine {
|
|
pub number: usize,
|
|
pub content: String,
|
|
pub highlighted_html: String,
|
|
pub is_folded: bool,
|
|
}
|
|
|
|
/// 代码文档
|
|
#[derive(Debug, Clone)]
|
|
pub struct CodeDocument {
|
|
pub title: String,
|
|
pub path: String,
|
|
pub language: CodeLanguage,
|
|
pub lines: Vec<CodeLine>,
|
|
pub total_lines: usize,
|
|
}
|
|
|
|
/// 代码阅读器
|
|
pub struct CodeReader {
|
|
syntax_set: SyntaxSet,
|
|
theme_set: ThemeSet,
|
|
}
|
|
|
|
impl CodeReader {
|
|
/// 创建代码阅读器
|
|
pub fn new() -> Result<Self> {
|
|
// 使用内置的语法和主题
|
|
let syntax_set = SyntaxSet::load_defaults_newlines();
|
|
let theme_set = ThemeSet::load_defaults();
|
|
|
|
Ok(Self {
|
|
syntax_set,
|
|
theme_set,
|
|
})
|
|
}
|
|
|
|
/// 解析代码文件
|
|
pub fn parse(&self, path: &str, content: &str) -> Result<CodeDocument> {
|
|
let path_obj = std::path::Path::new(path);
|
|
let ext = path_obj.extension()
|
|
.and_then(|e| e.to_str())
|
|
.unwrap_or("");
|
|
|
|
let language = CodeLanguage::from_extension(ext);
|
|
let title = path_obj.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("Untitled")
|
|
.to_string();
|
|
|
|
let lines = self.highlight_code(content, &language);
|
|
let total_lines = lines.len();
|
|
|
|
Ok(CodeDocument {
|
|
title,
|
|
path: path.to_string(),
|
|
language,
|
|
lines,
|
|
total_lines,
|
|
})
|
|
}
|
|
|
|
/// 语法高亮
|
|
fn highlight_code(&self, code: &str, language: &CodeLanguage) -> Vec<CodeLine> {
|
|
let syntax = self.syntax_set
|
|
.find_syntax_by_name(language.to_syntect_name())
|
|
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
|
|
|
|
let theme = &self.theme_set.themes["base16-ocean.dark"];
|
|
let mut highlighter = HighlightLines::new(syntax, theme);
|
|
|
|
let mut lines = Vec::new();
|
|
|
|
for (index, line) in code.lines().enumerate() {
|
|
let line_number = index + 1;
|
|
|
|
// 语法高亮
|
|
let ranges = highlighter.highlight_line(line, &self.syntax_set)
|
|
.unwrap_or_else(|_| vec![]);
|
|
|
|
// 转换为 HTML
|
|
let html = styled_line_to_highlighted_html(
|
|
&ranges[..],
|
|
IncludeBackground::No
|
|
).unwrap_or_else(|_| line.to_string());
|
|
|
|
lines.push(CodeLine {
|
|
number: line_number,
|
|
content: line.to_string(),
|
|
highlighted_html: html,
|
|
is_folded: false,
|
|
});
|
|
}
|
|
|
|
lines
|
|
}
|
|
|
|
/// 渲染代码文档为 HTML
|
|
pub fn render(&self, doc: &CodeDocument) -> Result<String> {
|
|
let mut html = String::new();
|
|
|
|
html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
|
|
html.push_str("<meta charset=\"UTF-8\">\n");
|
|
html.push_str(&format!("<title>{}</title>\n", doc.title));
|
|
html.push_str("<style>\n");
|
|
html.push_str(Self::get_code_css(&doc.language));
|
|
html.push_str("</style>\n</head>\n<body>\n");
|
|
|
|
html.push_str("<div class=\"code-container\">\n");
|
|
|
|
// 语言标识
|
|
html.push_str(&format!(
|
|
"<div class=\"code-header\">\n <span class=\"language-badge\">{}</span>\n <span class=\"line-count\">{} lines</span>\n</div>\n",
|
|
doc.language.display_name(),
|
|
doc.total_lines
|
|
));
|
|
|
|
// 代码内容
|
|
html.push_str("<pre class=\"code-content\"><code>\n");
|
|
|
|
for line in &doc.lines {
|
|
if line.is_folded {
|
|
continue;
|
|
}
|
|
|
|
html.push_str(&format!(
|
|
"<div class=\"code-line\" data-line=\"{}\">\n <span class=\"line-number\">{}</span>\n <span class=\"line-content\">{}</span>\n</div>\n",
|
|
line.number,
|
|
line.number,
|
|
line.highlighted_html
|
|
));
|
|
}
|
|
|
|
html.push_str("</code></pre>\n");
|
|
html.push_str("</div>\n");
|
|
|
|
html.push_str("</body>\n</html>");
|
|
|
|
Ok(html)
|
|
}
|
|
|
|
/// 获取代码样式
|
|
fn get_code_css(language: &CodeLanguage) -> &'static str {
|
|
r#"
|
|
:root {
|
|
--bg-primary: #1a1a2e;
|
|
--bg-secondary: #16213e;
|
|
--bg-line-number: #0f3460;
|
|
--text-primary: #eaeaea;
|
|
--text-muted: #6c757d;
|
|
--border-color: #2a2a4a;
|
|
--accent-color: #00adb5;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
}
|
|
.code-container {
|
|
max-width: 1200px;
|
|
margin: 20px auto;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.code-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 20px;
|
|
background: var(--bg-line-number);
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.language-badge {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--accent-color);
|
|
text-transform: uppercase;
|
|
}
|
|
.line-count {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.code-content {
|
|
overflow-x: auto;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.code-line {
|
|
display: flex;
|
|
min-height: 1.6em;
|
|
}
|
|
.code-line:hover {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
.line-number {
|
|
display: inline-block;
|
|
width: 50px;
|
|
padding: 0 10px;
|
|
text-align: right;
|
|
color: var(--text-muted);
|
|
background: var(--bg-line-number);
|
|
border-right: 1px solid var(--border-color);
|
|
user-select: none;
|
|
flex-shrink: 0;
|
|
}
|
|
.line-content {
|
|
flex: 1;
|
|
padding: 0 15px;
|
|
white-space: pre;
|
|
}
|
|
/* 语法高亮颜色 */
|
|
.c { color: #6c757d; font-style: italic; }
|
|
.k { color: #ff79c6; font-weight: bold; }
|
|
.o { color: #ff79c6; }
|
|
.cm { color: #6c757d; font-style: italic; }
|
|
.kd { color: #ff79c6; font-weight: bold; }
|
|
.kn { color: #ff79c6; font-weight: bold; }
|
|
.kp { color: #ff79c6; font-weight: bold; }
|
|
.kr { color: #ff79c6; font-weight: bold; }
|
|
.kt { color: #8be9fd; font-style: italic; }
|
|
.n { color: #f8f8f2; }
|
|
.na { color: #50fa7b; }
|
|
.nb { color: #8be9fd; font-style: italic; }
|
|
.nc { color: #50fa7b; font-weight: bold; }
|
|
.no { color: #f1fa8c; }
|
|
.nd { color: #bd93f9; }
|
|
.ni { color: #f8f8f2; }
|
|
.ne { color: #50fa7b; font-weight: bold; }
|
|
.nf { color: #50fa7b; }
|
|
.nl { color: #8be9fd; font-style: italic; }
|
|
.nn { color: #f8f8f2; }
|
|
.nt { color: #ff79c6; }
|
|
.nv { color: #f8f8f2; }
|
|
.s { color: #f1fa8c; }
|
|
.s1 { color: #f1fa8c; }
|
|
.s2 { color: #f1fa8c; }
|
|
.se { color: #f1fa8c; }
|
|
.sh { color: #f1fa8c; }
|
|
.si { color: #f1fa8c; }
|
|
.sx { color: #f1fa8c; }
|
|
.m { color: #bd93f9; }
|
|
.mi { color: #bd93f9; }
|
|
.mf { color: #bd93f9; }
|
|
/* 滚动条 */
|
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
|
::-webkit-scrollbar-thumb { background: var(--bg-line-number); border-radius: 5px; }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--accent-color); }
|
|
"#
|
|
}
|
|
|
|
/// 折叠代码行
|
|
pub fn fold_lines(&mut self, doc: &mut CodeDocument, start: usize, end: usize) {
|
|
for line in &mut doc.lines {
|
|
if line.number >= start && line.number <= end {
|
|
line.is_folded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 搜索代码
|
|
pub fn search(&self, doc: &CodeDocument, query: &str) -> Vec<usize> {
|
|
let mut results = Vec::new();
|
|
let query_lower = query.to_lowercase();
|
|
|
|
for line in &doc.lines {
|
|
if line.content.to_lowercase().contains(&query_lower) {
|
|
results.push(line.number);
|
|
}
|
|
}
|
|
|
|
results
|
|
}
|
|
}
|
|
|
|
impl Default for CodeReader {
|
|
fn default() -> Self {
|
|
Self::new().expect("Failed to create CodeReader")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_language_detection() {
|
|
assert!(matches!(CodeLanguage::from_extension("rs"), CodeLanguage::Rust));
|
|
assert!(matches!(CodeLanguage::from_extension("py"), CodeLanguage::Python));
|
|
assert!(matches!(CodeLanguage::from_extension("unknown"), CodeLanguage::Unknown));
|
|
}
|
|
|
|
#[test]
|
|
fn test_code_reader_creation() {
|
|
let reader = CodeReader::new();
|
|
assert!(reader.is_ok());
|
|
}
|
|
}
|