Skip to main content

nargo_document/config/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::{collections::HashMap, fs::File, io::Read, path::Path};
3
4/// 配置加载和验证相关的错误类型
5#[derive(Debug)]
6pub enum ConfigError {
7    /// 文件读取错误
8    FileReadError(std::io::Error),
9    /// JSON 解析错误
10    JsonParseError(serde_json::Error),
11    /// TOML 解析错误
12    TomlParseError(toml::de::Error),
13    /// 配置验证错误
14    ValidationError(String),
15    /// 不支持的配置文件格式
16    UnsupportedFormat(String),
17}
18
19impl std::error::Error for ConfigError {
20    fn description(&self) -> &str {
21        match self {
22            ConfigError::FileReadError(_) => "Failed to read config file",
23            ConfigError::JsonParseError(_) => "Failed to parse JSON config",
24            ConfigError::TomlParseError(_) => "Failed to parse TOML config",
25            ConfigError::ValidationError(_) => "Config validation error",
26            ConfigError::UnsupportedFormat(_) => "Unsupported config file format",
27        }
28    }
29}
30
31impl std::fmt::Display for ConfigError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            ConfigError::FileReadError(err) => write!(f, "Failed to read config file: {}", err),
35            ConfigError::JsonParseError(err) => write!(f, "Failed to parse JSON config: {}", err),
36            ConfigError::TomlParseError(err) => write!(f, "Failed to parse TOML config: {}", err),
37            ConfigError::ValidationError(msg) => write!(f, "Config validation error: {}", msg),
38            ConfigError::UnsupportedFormat(fmt) => write!(f, "Unsupported config file format: {}", fmt),
39        }
40    }
41}
42
43impl From<std::io::Error> for ConfigError {
44    fn from(err: std::io::Error) -> Self {
45        ConfigError::FileReadError(err)
46    }
47}
48
49impl From<serde_json::Error> for ConfigError {
50    fn from(err: serde_json::Error) -> Self {
51        ConfigError::JsonParseError(err)
52    }
53}
54
55impl From<toml::de::Error> for ConfigError {
56    fn from(err: toml::de::Error) -> Self {
57        ConfigError::TomlParseError(err)
58    }
59}
60
61/// 配置验证 trait
62pub trait ConfigValidation {
63    /// 验证配置的有效性
64    ///
65    /// # Errors
66    ///
67    /// 返回 `ConfigError::ValidationError` 如果配置无效
68    fn validate(&self) -> Result<(), ConfigError>;
69}
70
71/// Nargo Document 配置 - 兼容 VuTeX 配置格式
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
73pub struct Config {
74    /// 站点标题
75    pub title: Option<String>,
76    /// 站点描述
77    pub description: Option<String>,
78    /// 基础路径
79    pub base: Option<String>,
80    /// 语言配置
81    pub locales: HashMap<String, LocaleConfig>,
82    /// 主题配置
83    pub theme: ThemeConfig,
84    /// 插件配置
85    pub plugins: Vec<PluginConfig>,
86    /// Markdown 配置
87    pub markdown: MarkdownConfig,
88    /// 构建配置
89    pub build: BuildConfig,
90}
91
92impl Config {
93    /// 从文件加载配置,根据文件扩展名自动选择解析器
94    ///
95    /// # Arguments
96    ///
97    /// * `path` - 配置文件的路径
98    ///
99    /// # Errors
100    ///
101    /// 返回 `ConfigError` 如果文件读取或解析失败
102    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
103        let path = path.as_ref();
104        let content = std::fs::read_to_string(path)?;
105
106        match path.extension().and_then(|ext| ext.to_str()) {
107            Some("json") => Self::load_from_json_str(&content),
108            Some("toml") => Self::load_from_toml_str(&content),
109            Some(ext) => Err(ConfigError::UnsupportedFormat(ext.to_string())),
110            None => Err(ConfigError::UnsupportedFormat("no extension".to_string())),
111        }
112    }
113
114    /// 从 JSON 字符串加载配置
115    ///
116    /// # Arguments
117    ///
118    /// * `json_str` - JSON 格式的配置字符串
119    ///
120    /// # Errors
121    ///
122    /// 返回 `ConfigError::JsonParseError` 如果 JSON 解析失败
123    pub fn load_from_json_str(json_str: &str) -> Result<Self, ConfigError> {
124        let config: Self = serde_json::from_str(json_str)?;
125        config.validate()?;
126        Ok(config)
127    }
128
129    /// 从 TOML 字符串加载配置
130    ///
131    /// # Arguments
132    ///
133    /// * `toml_str` - TOML 格式的配置字符串
134    ///
135    /// # Errors
136    ///
137    /// 返回 `ConfigError::TomlParseError` 如果 TOML 解析失败
138    pub fn load_from_toml_str(toml_str: &str) -> Result<Self, ConfigError> {
139        let config: Self = toml::from_str(toml_str)?;
140        config.validate()?;
141        Ok(config)
142    }
143
144    /// 从目录中查找并加载配置文件
145    ///
146    /// 按以下顺序查找配置文件:
147    /// 1. nargodoc.config.toml
148    /// 2. nargodoc.config.json
149    /// 3. vutex.config.toml (兼容)
150    /// 4. vutex.config.json (兼容)
151    ///
152    /// # Arguments
153    ///
154    /// * `dir` - 要搜索的目录路径
155    ///
156    /// # Errors
157    ///
158    /// 返回 `ConfigError` 如果配置文件读取或解析失败
159    pub fn load_from_dir<P: AsRef<Path>>(dir: P) -> Result<Self, ConfigError> {
160        let dir = dir.as_ref();
161
162        let toml_path = dir.join("nargodoc.config.toml");
163        if toml_path.exists() {
164            return Self::load_from_file(toml_path);
165        }
166
167        let json_path = dir.join("nargodoc.config.json");
168        if json_path.exists() {
169            return Self::load_from_file(json_path);
170        }
171
172        let vutex_toml_path = dir.join("vutex.config.toml");
173        if vutex_toml_path.exists() {
174            return Self::load_from_file(vutex_toml_path);
175        }
176
177        let vutex_json_path = dir.join("vutex.config.json");
178        if vutex_json_path.exists() {
179            return Self::load_from_file(vutex_json_path);
180        }
181
182        Ok(Self::default())
183    }
184
185    /// 将配置序列化为 JSON 字符串
186    ///
187    /// # Errors
188    ///
189    /// 返回 `serde_json::Error` 如果序列化失败
190    pub fn to_json(&self) -> Result<String, serde_json::Error> {
191        serde_json::to_string_pretty(self)
192    }
193
194    /// 将配置序列化为 TOML 字符串
195    ///
196    /// # Errors
197    ///
198    /// 返回 `toml::ser::Error` 如果序列化失败
199    pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
200        toml::to_string_pretty(self)
201    }
202
203    /// 创建新的配置
204    pub fn new() -> Self {
205        Self::default()
206    }
207
208    /// 设置站点标题
209    pub fn with_title(mut self, title: String) -> Self {
210        self.title = Some(title);
211        self
212    }
213
214    /// 设置站点描述
215    pub fn with_description(mut self, description: String) -> Self {
216        self.description = Some(description);
217        self
218    }
219
220    /// 添加语言配置
221    pub fn add_locale(mut self, lang: String, config: LocaleConfig) -> Self {
222        self.locales.insert(lang, config);
223        self
224    }
225}
226
227impl ConfigValidation for Config {
228    fn validate(&self) -> Result<(), ConfigError> {
229        let default_count = self.locales.iter().filter(|(_, cfg)| cfg.default.unwrap_or(false)).count();
230        if default_count > 1 {
231            return Err(ConfigError::ValidationError(format!("Multiple default locales specified: found {} default locales", default_count)));
232        }
233
234        for (lang_code, locale) in &self.locales {
235            if lang_code.is_empty() {
236                return Err(ConfigError::ValidationError("Locale code cannot be empty".to_string()));
237            }
238            locale.validate()?;
239        }
240
241        self.theme.validate()?;
242
243        for (i, plugin) in self.plugins.iter().enumerate() {
244            if plugin.name.is_empty() {
245                return Err(ConfigError::ValidationError(format!("Plugin at index {} has empty name", i)));
246            }
247        }
248
249        self.markdown.validate()?;
250        self.build.validate()?;
251
252        Ok(())
253    }
254}
255
256/// 语言配置
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
258pub struct LocaleConfig {
259    /// 语言标签
260    pub label: String,
261    /// 语言描述
262    pub description: Option<String>,
263    /// 语言链接
264    pub link: Option<String>,
265    /// 是否为默认语言
266    pub default: Option<bool>,
267    /// 导航栏配置(语言特定)
268    pub nav: Option<Vec<NavItem>>,
269    /// 侧边栏配置(语言特定)
270    pub sidebar: Option<HashMap<String, Vec<SidebarItem>>>,
271}
272
273impl LocaleConfig {
274    /// 创建新的语言配置
275    pub fn new(label: String) -> Self {
276        Self { label, description: None, link: None, default: None, nav: None, sidebar: None }
277    }
278
279    /// 设置为默认语言
280    pub fn with_default(mut self, is_default: bool) -> Self {
281        self.default = Some(is_default);
282        self
283    }
284
285    /// 设置导航栏配置
286    pub fn with_nav(mut self, nav: Vec<NavItem>) -> Self {
287        self.nav = Some(nav);
288        self
289    }
290
291    /// 设置侧边栏配置
292    pub fn with_sidebar(mut self, sidebar: HashMap<String, Vec<SidebarItem>>) -> Self {
293        self.sidebar = Some(sidebar);
294        self
295    }
296}
297
298impl ConfigValidation for LocaleConfig {
299    fn validate(&self) -> Result<(), ConfigError> {
300        if self.label.is_empty() {
301            return Err(ConfigError::ValidationError("Locale label cannot be empty".to_string()));
302        }
303
304        if let Some(nav) = &self.nav {
305            for (i, item) in nav.iter().enumerate() {
306                item.validate().map_err(|e| ConfigError::ValidationError(format!("Nav item at index {}: {}", i, e)))?;
307            }
308        }
309
310        if let Some(sidebar) = &self.sidebar {
311            for (group_key, items) in sidebar {
312                for (i, item) in items.iter().enumerate() {
313                    item.validate().map_err(|e| ConfigError::ValidationError(format!("Sidebar item in group '{}' at index {}: {}", group_key, i, e)))?;
314                }
315            }
316        }
317
318        Ok(())
319    }
320}
321
322/// 主题配置
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
324pub struct ThemeConfig {
325    /// 导航栏配置
326    pub nav: Vec<NavItem>,
327    /// 侧边栏配置
328    pub sidebar: HashMap<String, Vec<SidebarItem>>,
329    /// 社交链接
330    pub social_links: Vec<SocialLink>,
331    /// 页脚配置
332    pub footer: Option<FooterConfig>,
333    /// 自定义配置
334    pub custom: HashMap<String, serde_json::Value>,
335}
336
337impl ThemeConfig {
338    /// 创建新的主题配置
339    pub fn new() -> Self {
340        Self::default()
341    }
342
343    /// 添加导航栏项
344    pub fn add_nav_item(mut self, item: NavItem) -> Self {
345        self.nav.push(item);
346        self
347    }
348}
349
350impl ConfigValidation for ThemeConfig {
351    fn validate(&self) -> Result<(), ConfigError> {
352        for (i, item) in self.nav.iter().enumerate() {
353            item.validate().map_err(|e| ConfigError::ValidationError(format!("Theme nav item at index {}: {}", i, e)))?;
354        }
355
356        for (group_key, items) in &self.sidebar {
357            for (i, item) in items.iter().enumerate() {
358                item.validate().map_err(|e| ConfigError::ValidationError(format!("Theme sidebar item in group '{}' at index {}: {}", group_key, i, e)))?;
359            }
360        }
361
362        for (i, link) in self.social_links.iter().enumerate() {
363            if link.platform.is_empty() {
364                return Err(ConfigError::ValidationError(format!("Social link at index {} has empty platform name", i)));
365            }
366            if link.link.is_empty() {
367                return Err(ConfigError::ValidationError(format!("Social link at index {} has empty URL", i)));
368            }
369        }
370
371        Ok(())
372    }
373}
374
375/// 导航栏项
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct NavItem {
378    /// 显示文本
379    pub text: String,
380    /// 链接
381    pub link: Option<String>,
382    /// 子项
383    pub items: Option<Vec<NavItem>>,
384}
385
386impl NavItem {
387    /// 创建新的导航栏项
388    pub fn new(text: String) -> Self {
389        Self { text, link: None, items: None }
390    }
391
392    /// 设置链接
393    pub fn with_link(mut self, link: String) -> Self {
394        self.link = Some(link);
395        self
396    }
397
398    /// 添加子项
399    pub fn add_item(mut self, item: NavItem) -> Self {
400        if self.items.is_none() {
401            self.items = Some(Vec::new());
402        }
403        if let Some(items) = &mut self.items {
404            items.push(item);
405        }
406        self
407    }
408}
409
410impl ConfigValidation for NavItem {
411    fn validate(&self) -> Result<(), ConfigError> {
412        if self.text.is_empty() {
413            return Err(ConfigError::ValidationError("Nav item text cannot be empty".to_string()));
414        }
415
416        if let Some(items) = &self.items {
417            for (i, item) in items.iter().enumerate() {
418                item.validate().map_err(|e| ConfigError::ValidationError(format!("Sub-item at index {}: {}", i, e)))?;
419            }
420        }
421
422        Ok(())
423    }
424}
425
426/// 侧边栏项
427#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
428pub struct SidebarItem {
429    /// 显示文本
430    pub text: String,
431    /// 链接
432    pub link: Option<String>,
433    /// 子项
434    pub items: Option<Vec<SidebarItem>>,
435    /// 是否折叠
436    pub collapsed: Option<bool>,
437}
438
439impl SidebarItem {
440    /// 创建新的侧边栏项
441    pub fn new(text: String) -> Self {
442        Self { text, link: None, items: None, collapsed: None }
443    }
444
445    /// 设置链接
446    pub fn with_link(mut self, link: String) -> Self {
447        self.link = Some(link);
448        self
449    }
450}
451
452impl ConfigValidation for SidebarItem {
453    fn validate(&self) -> Result<(), ConfigError> {
454        if self.text.is_empty() {
455            return Err(ConfigError::ValidationError("Sidebar item text cannot be empty".to_string()));
456        }
457
458        if let Some(items) = &self.items {
459            for (i, item) in items.iter().enumerate() {
460                item.validate().map_err(|e| ConfigError::ValidationError(format!("Sub-item at index {}: {}", i, e)))?;
461            }
462        }
463
464        Ok(())
465    }
466}
467
468/// 社交链接
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
470pub struct SocialLink {
471    /// 平台名称
472    pub platform: String,
473    /// 链接
474    pub link: String,
475}
476
477/// 页脚配置
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
479pub struct FooterConfig {
480    /// 版权信息
481    pub copyright: Option<String>,
482    /// 页脚消息
483    pub message: Option<String>,
484}
485
486/// 插件配置
487#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
488pub struct PluginConfig {
489    /// 插件名称
490    pub name: String,
491    /// 插件配置
492    pub options: HashMap<String, serde_json::Value>,
493}
494
495/// Markdown 配置
496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
497pub struct MarkdownConfig {
498    /// 是否启用行号
499    pub line_numbers: bool,
500    /// 代码主题
501    pub code_theme: Option<String>,
502    /// 自定义配置
503    pub custom: HashMap<String, serde_json::Value>,
504}
505
506impl ConfigValidation for MarkdownConfig {
507    fn validate(&self) -> Result<(), ConfigError> {
508        Ok(())
509    }
510}
511
512/// 构建配置
513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
514pub struct BuildConfig {
515    /// 输出目录
516    pub out_dir: Option<String>,
517    /// 源目录
518    pub src_dir: Option<String>,
519    /// 是否启用清理
520    pub clean: bool,
521    /// 是否启用压缩
522    pub minify: bool,
523}
524
525impl ConfigValidation for BuildConfig {
526    fn validate(&self) -> Result<(), ConfigError> {
527        Ok(())
528    }
529}
530
531/// 兼容旧版 HXO Document 的 Locale 结构
532#[derive(Debug, Deserialize, Serialize, Clone)]
533pub struct LegacyLocale {
534    pub label: String,
535    pub lang: String,
536    pub link: String,
537    pub theme_config: Option<ThemeConfig>,
538}
539
540/// 兼容旧版 HXO Document 的 Footer 结构
541#[derive(Debug, Deserialize, Serialize, Clone)]
542pub struct LegacyFooter {
543    pub message: String,
544    pub copyright: String,
545}
546
547/// 兼容旧版 HXO Document 的 MarkdownTheme 结构
548#[derive(Debug, Deserialize, Serialize, Clone)]
549pub struct LegacyMarkdownTheme {
550    pub light: String,
551    pub dark: String,
552}
553
554/// 兼容旧版 HXO Document 的 MarkdownConfig 结构
555#[derive(Debug, Deserialize, Serialize, Clone)]
556pub struct LegacyMarkdownConfig {
557    pub theme: Option<LegacyMarkdownTheme>,
558    pub shiki_setup: Option<serde_json::Value>,
559}
560
561/// 兼容旧版 HXO Document 的 BuildConfig 结构
562#[derive(Debug, Deserialize, Serialize, Clone)]
563pub struct LegacyBuildConfig {
564    pub out_dir: Option<String>,
565    pub base: Option<String>,
566}
567
568/// 兼容旧版 HXO Document 的配置结构
569#[derive(Debug, Deserialize, Serialize, Clone)]
570pub struct LegacyConfig {
571    pub title: String,
572    pub description: String,
573    pub locales: Option<Vec<LegacyLocale>>,
574    pub theme: Option<String>,
575    pub theme_config: Option<ThemeConfig>,
576    pub markdown: Option<LegacyMarkdownConfig>,
577    pub build: Option<LegacyBuildConfig>,
578}