//! Concept documentation and the extracted `apsis-rs` interface catalog. //! //! The module owns everything the service knows about documents: //! //! - [`content`] makes the embedded `content/` files available. //! - [`markdown`] turns a document body into HTML. //! - [`document`] models a document, the chapter tree, and the search index. //! - [`api`] models the interface catalog extracted from the `apsis-rs` //! sources. //! //! [`load_documents`] and [`load_api_catalog`] assemble the two collections //! from the embedded content at startup. //! //! A document is written once in English as `concepts/.md`. A Chinese //! translation is the file `concepts/.zh.md`, whose metadata and body //! replace the English ones for a reader whose language is Chinese. A //! document without a translation is served in English to every reader. //! //! 概念文档,以及从 `apsis-rs` 提取的接口目录。 //! //! 服务关于文档所知的一切都归本模块所有: //! //! - [`content`] 使内嵌的 `content/` 文件可用。 //! - [`markdown`] 把文档正文转为 HTML。 //! - [`document`] 为文档、章节树与搜索索引建模。 //! - [`api`] 为从 `apsis-rs` 源码提取的接口目录建模。 //! //! [`load_documents`] 与 [`load_api_catalog`] 在启动时从内嵌内容组装这两个集合。 //! //! 一篇文档以英文写一次,为 `concepts/.md`。中文译文是 //! `concepts/.zh.md`,其元数据与正文对语言为中文的读者取代英文部分。没有译文 //! 的文档对所有读者都以英文提供。 pub mod api; pub mod content; pub mod document; pub mod markdown; pub mod mathml; #[allow(unused_imports)] pub use api::ApiItem; pub use api::{ApiCatalog, ApiCrate}; pub use document::{Document, DocumentVariant, Language, SearchIndex, Tree}; /// Parses every embedded concept document, merging the translations. /// /// A file that fails to parse is skipped; the service reports the remaining /// documents and the document route answers with a not found status for the /// missing one. /// /// 解析每一篇内嵌概念文档,并合并译文。 /// /// 解析失败的文件被跳过;服务报告其余文档,而对缺失的那一篇,文档路由以「未找到」 /// 状态作答。 pub fn load_documents() -> Vec { // The English files are the base of the collection. The identifier is // the file stem with the language suffix removed, so a translation and // its original meet on the same key. // 英文文件是集合的基础。标识是去掉语言后缀的文件名主干,因此译文与其原文 // 在同一个键上相遇。 let mut order: Vec = Vec::new(); let mut documents: std::collections::HashMap = std::collections::HashMap::new(); for (path, text) in content::list("concepts/", ".md") { let Some(relative) = path.strip_prefix("concepts/") else { continue; }; // The identifier is the file name; the directory above it is the // layer, which is a separate piece of information. // 标识即文件名;其上一级目录是层,那是另一份信息。 let Some(stem) = relative .rsplit_once('/') .map_or(Some(relative), |(_, name)| Some(name)) else { continue; }; let Some(stem) = stem.strip_suffix(".md") else { continue; }; let language = Language::of_file(stem); if language != Language::English { continue; } let layer = crate::docs::document::layer_of(relative); if let Some(document) = Document::parse(stem, &layer, Language::English, text) { order.push(stem.to_string()); documents.insert(stem.to_string(), document); } } for (path, text) in content::list("concepts/", ".zh.md") { let Some(stem) = path .strip_prefix("concepts/") .and_then(|name| { name.rsplit_once('/') .map_or(Some(name), |(_, base)| Some(base)) }) .and_then(|name| name.strip_suffix(".zh.md")) else { continue; }; // A translation without an English original is ignored rather than // served on its own, so that the collection always has one entry per // identifier. // 没有英文原文的译文被忽略,而不单独提供,这样集合中每个标识始终只有 // 一个条目。 if !documents.contains_key(stem) { continue; } let Some(translation) = DocumentVariant::parse(text) else { continue; }; let tags_zh = crate::docs::document::tags_of(text); if let Some(document) = documents.get_mut(stem) { document.chinese = Some(translation); document.tags_zh = tags_zh; } } order .into_iter() .filter_map(|id| documents.remove(&id)) .collect() } /// Parses the embedded interface catalog, applying the translation when /// one is embedded. /// /// 解析内嵌的接口目录。 pub fn load_api_catalog() -> ApiCatalog { let Some(text) = content::get("api.json") else { return ApiCatalog { crates: Vec::new(), generated_from: Vec::new(), item_names: std::collections::HashSet::new(), }; }; api::parse(text) }