readme_sync/
cmark_docs.rs

1use std::borrow::Cow;
2use std::path::Path;
3use std::sync::Arc;
4
5use pulldown_cmark::Event;
6use thiserror::Error;
7
8use crate::{
9    CMarkData, CMarkDataIter, Config, DisallowUrlsWithPrefixError, File, FileDocs,
10    FileDocsFromFileError, FileFromPathError, Manifest, Package,
11};
12
13/// Parsed documentation Markdown with optionally specified package path and package manifest.
14#[derive(Clone, Debug, Default, PartialEq)]
15pub struct CMarkDocs<P, M> {
16    data: CMarkData,
17    package_path: P,
18    manifest: M,
19}
20
21impl<'a> CMarkDocs<&'a Path, &'a Manifest> {
22    /// Creates docs from package and default config.
23    ///
24    /// First it reads docs file by path specified in the package manifest.
25    /// Then it parses it with default configuration.
26    pub fn from_package_with_default_config(
27        package: &'a Package,
28    ) -> Result<Self, CMarkDocsFromPackageError> {
29        Self::from_package_and_config(package, &Config::default())
30    }
31
32    /// Creates docs from package and the specified config.
33    ///
34    /// First it reads docs file by path specified in the package manifest.
35    /// Then it parses it with the specified configuration.
36    pub fn from_package_and_config(
37        package: &'a Package,
38        config: &Config<'_>,
39    ) -> Result<Self, CMarkDocsFromPackageError> {
40        let path = package.manifest().default_relative_target_path();
41        let file = Arc::new(File::from_path(path.to_path_buf(), Some(package.path()))?);
42        let package_path = package.path();
43        let manifest = package.manifest();
44        Ok(Self::from_file_and_config_and_package_path_and_manifest(
45            file,
46            config,
47            package_path,
48            manifest,
49        )?)
50    }
51}
52
53impl CMarkDocs<(), ()> {
54    /// Creates docs from file and the specified config.
55    ///
56    /// The method parses a file with the specified configuration.
57    pub fn from_file_and_config(
58        file: Arc<File>,
59        config: &Config<'_>,
60    ) -> Result<Self, FileDocsFromFileError> {
61        Self::from_file_and_config_and_package_path_and_manifest(file, config, (), ())
62    }
63}
64
65impl<'a, P, M> CMarkDocs<P, M> {
66    /// Adding the specified package path to the docs.
67    pub fn with_package_path(self, package_path: &'a Package) -> CMarkDocs<&'a Package, M> {
68        CMarkDocs {
69            data: self.data,
70            package_path,
71            manifest: self.manifest,
72        }
73    }
74
75    /// Adding the specified manifest to the docs.
76    pub fn with_manifest(self, manifest: &'a Manifest) -> CMarkDocs<P, &'a Manifest> {
77        CMarkDocs {
78            data: self.data,
79            package_path: self.package_path,
80            manifest,
81        }
82    }
83
84    /// Creates docs from file, config, package path and manifest.
85    pub fn from_file_and_config_and_package_path_and_manifest(
86        file: Arc<File>,
87        config: &Config<'_>,
88        package_path: P,
89        manifest: M,
90    ) -> Result<Self, FileDocsFromFileError> {
91        let file_docs = Arc::new(FileDocs::from_file(file, config)?);
92        Ok(Self::from_file_docs_and_package_path_and_manifest(
93            file_docs,
94            package_path,
95            manifest,
96        ))
97    }
98
99    /// Creates docs from file docs content, package path and manifest.
100    pub fn from_file_docs_and_package_path_and_manifest(
101        file_docs: Arc<FileDocs>,
102        package_path: P,
103        manifest: M,
104    ) -> Self {
105        let data = CMarkData::from_file_docs(file_docs);
106        Self::from_data_chunks_package_pach_and_manifest(data, package_path, manifest)
107    }
108
109    /// Creates docs from CMark items, package path and manifest.
110    pub fn from_data_chunks_package_pach_and_manifest(
111        data: CMarkData,
112        package_path: P,
113        manifest: M,
114    ) -> Self {
115        Self {
116            data,
117            package_path,
118            manifest,
119        }
120    }
121
122    /// Returns CMark items.
123    pub fn data(&self) -> &CMarkData {
124        &self.data
125    }
126
127    /// Consumes the `CMarkDocs`, returning `CMarkData`.
128    pub fn into_data(self) -> CMarkData {
129        self.data
130    }
131
132    /// Returns the package path.
133    pub fn package_path(&self) -> &P {
134        &self.package_path
135    }
136
137    /// Returns the manifest.
138    pub fn manifest(&self) -> &M {
139        &self.manifest
140    }
141
142    /// Iterate over `CMarkItem`s.
143    pub fn iter(&self) -> CMarkDataIter<'_> {
144        self.data.iter()
145    }
146
147    /// Iterate over pulldown-cmark events.
148    pub fn iter_events(&self) -> impl Iterator<Item = &Event<'_>> {
149        self.data.iter().filter_map(|item| item.event())
150    }
151
152    fn map<F>(mut self, func: F) -> CMarkDocs<P, M>
153    where
154        F: FnOnce(CMarkData) -> CMarkData,
155    {
156        self.data = func(self.data);
157        self
158    }
159
160    fn map_result<F, E>(mut self, func: F) -> Result<CMarkDocs<P, M>, E>
161    where
162        F: FnOnce(CMarkData) -> Result<CMarkData, E>,
163    {
164        self.data = func(self.data)?;
165        Ok(self)
166    }
167
168    /// Concatenate adjacent text events.
169    ///
170    /// Use this transformation if you deleted some nodes manually
171    /// and want to merge the neighboring text nodes.
172    ///
173    /// This transformation is always applied right after
174    /// readme and docs parsing, because some text events remain ununited.
175    /// For example Rust attribute parser generate seperate text events
176    /// for every line of source code, and pulldown_cmark generate
177    /// seperate text events for character entity reference.
178    pub fn concat_texts(self) -> CMarkDocs<P, M> {
179        self.map(|data| data.concat_texts())
180    }
181
182    /// Increment levels of all headings.
183    ///
184    /// In readme, the first level heading is usually used only for the project title.
185    /// The second level header is usually used in for text section headings in readme.
186    /// Rustdoc automatically adds the header of a crate name and the first level headers are used for text sections.
187    ///
188    /// So it is necessary to increase the level of all headings in the documentation in order to synchronize the headings.
189    pub fn increment_heading_levels(self) -> CMarkDocs<P, M> {
190        self.map(|data| data.increment_heading_levels())
191    }
192
193    /// Add a first level heading with the specified text.
194    ///
195    /// This function could be useful after heading level incremented.
196    pub fn add_title(self, text: &str) -> CMarkDocs<P, M> {
197        self.map(|data| data.add_title(text))
198    }
199
200    /// Remove section with the specified heading text and level and its subsections.
201    pub fn remove_section(self, heading: &str, level: u32) -> Self {
202        self.map(|data| data.remove_section(heading, level))
203    }
204
205    /// Remove the specified fenced code block tag.
206    pub fn remove_codeblock_tag(self, tag: &str) -> CMarkDocs<P, M> {
207        self.map(|data| data.remove_codeblock_tag(tag))
208    }
209
210    /// Remove the specified fenced code block tags.
211    pub fn remove_codeblock_tags(self, tags: &[&str]) -> CMarkDocs<P, M> {
212        self.map(|data| data.remove_codeblock_tags(tags))
213    }
214
215    /// Remove fenced code block tags that are used by `cargo test`.
216    ///
217    /// See <https://doc.rust-lang.org/rustdoc/documentation-tests.html> for more details.
218    pub fn remove_codeblock_rust_test_tags(self) -> CMarkDocs<P, M> {
219        self.map(|data| data.remove_codeblock_rust_test_tags())
220    }
221
222    /// Use the specified codeblock tag, if they are not specified
223    pub fn use_default_codeblock_tag(self, tag: &str) -> CMarkDocs<P, M> {
224        self.map(|data| data.use_default_codeblock_tag(tag))
225    }
226
227    /// Use rust fenced codeblock highlight as default.
228    pub fn use_default_codeblock_rust_tag(self) -> CMarkDocs<P, M> {
229        self.map(|data| data.use_default_codeblock_rust_tag())
230    }
231
232    /// Remove hidden rust code from rust fenced codeblocks.
233    ///
234    /// See <https://doc.rust-lang.org/rustdoc/documentation-tests.html#hiding-portions-of-the-example> for more details.
235    pub fn remove_hidden_rust_code(self) -> CMarkDocs<P, M> {
236        self.map(|data| data.remove_hidden_rust_code())
237    }
238
239    /// Returns self if absolute docs links to the specified repository not found,
240    /// otherwise returns an error.
241    pub fn disallow_absolute_docs_links(
242        self,
243        package_name: &str,
244        documentation_url: &str,
245    ) -> Result<CMarkDocs<P, M>, DisallowUrlsWithPrefixError> {
246        self.map_result(|data| data.disallow_absolute_docs_links(package_name, documentation_url))
247    }
248
249    /// Convert all relative links into absolute ones using
250    /// the specified package documentation url as the root address.
251    pub fn use_absolute_docs_urls(
252        self,
253        package_name: &str,
254        documentation_url: &str,
255    ) -> CMarkDocs<P, M> {
256        self.map(|data| data.use_absolute_docs_urls(package_name, documentation_url))
257    }
258}
259
260impl<'a, P> CMarkDocs<P, &'a Manifest> {
261    /// Add a first level heading with the manifest package name.
262    ///
263    /// This function could be useful after heading level incremented.
264    pub fn add_package_title(self) -> CMarkDocs<P, &'a Manifest> {
265        let name = self.manifest.package.name.clone();
266        self.add_title(&name)
267    }
268
269    /// Returns self if absolute docs links to the manifest repository not found,
270    /// otherwise returns an error.
271    pub fn disallow_absolute_package_docs_links(
272        self,
273    ) -> Result<CMarkDocs<P, &'a Manifest>, DisallowAbsolutePackageDocsLinksError> {
274        let name = self.manifest.package.name.clone();
275        let documentation = self
276            .manifest
277            .package
278            .documentation
279            .clone()
280            .ok_or(DisallowAbsolutePackageDocsLinksError::DocsUrlNotFound)?;
281        Ok(self.disallow_absolute_docs_links(&name, &documentation)?)
282    }
283
284    /// Convert all relative links into absolute ones
285    /// using the manifest package documentation url as the root address.
286    pub fn use_absolute_package_docs_urls(
287        self,
288    ) -> Result<CMarkDocs<P, &'a Manifest>, UseAbsolutePackageDocsUrlsError> {
289        let name = self.manifest.package.name.clone();
290        let documentation = self
291            .manifest
292            .package
293            .documentation
294            .clone()
295            .ok_or(UseAbsolutePackageDocsUrlsError::DocsUrlNotFound)?;
296        Ok(self.use_absolute_docs_urls(&name, &documentation))
297    }
298
299    /// Converts all links with function `func` applied to each link address.
300    pub fn map_links<F>(self, func: F, note: impl Into<Cow<'static, str>>) -> Self
301    where
302        for<'b> F: FnMut(&'b str) -> Cow<'b, str>,
303    {
304        self.map(|data| data.map_links(func, note))
305    }
306}
307
308/// An error which can occur when creating docs from package.
309#[derive(Debug, Error)]
310pub enum CMarkDocsFromPackageError {
311    /// File reading failed.
312    #[error(transparent)]
313    FileError(#[from] FileFromPathError),
314    /// Documentation parsing failed.
315    #[error("Documentation parsing failed: {0}")]
316    ParseError(#[from] FileDocsFromFileError),
317}
318
319/// An error which can occur when checking for disallowed absolute package docs links.
320#[derive(Clone, Debug, Error)]
321pub enum DisallowAbsolutePackageDocsLinksError {
322    /// A disallowed prefix found.
323    #[error(transparent)]
324    DisallowUrlsWithPrefixError(#[from] DisallowUrlsWithPrefixError),
325    /// Manifest does not contain `package.documentation` field.
326    #[error("Manifest does not contain package.documentation field")]
327    DocsUrlNotFound,
328}
329
330/// An error which can occur when converting relative links into absolute ones,
331/// using the manifest package documentation url as the root address.
332#[derive(Clone, Copy, Debug, Error)]
333pub enum UseAbsolutePackageDocsUrlsError {
334    #[error("Manifest does not contain package.documentation field")]
335    DocsUrlNotFound,
336}