Files
readflow/src/core/code_reader.rs
大麦 600f205c87 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 核心功能全部完成!
2026-03-10 14:29:56 +08:00

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());
}
}