feat: 完成 Phase 2-4 核心功能
## 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 核心功能全部完成!
This commit is contained in:
428
src/core/code_reader.rs
Normal file
428
src/core/code_reader.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
//! 代码阅读器模块
|
||||
//!
|
||||
//! 支持语法高亮、代码折叠、行号显示等功能
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user