readme_sync/
cmark_readme.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use pulldown_cmark::Event;
5use thiserror::Error;
6
7use crate::{
8    CMarkData, CMarkDataIter, DisallowUrlsWithPrefixError, File, FileFromPathError, Manifest,
9    Package,
10};
11
12/// Parsed readme Markdown with optionally specified package path and package manifest.
13#[derive(Clone, Debug, Default, PartialEq)]
14pub struct CMarkReadme<P, M> {
15    data: CMarkData,
16    package_path: P,
17    manifest: M,
18}
19
20impl<'a> CMarkReadme<&'a Path, &'a Manifest> {
21    /// Creates readme from package.
22    ///
23    /// It reads readme file by path specified in the package manifest.
24    pub fn from_package(package: &'a Package) -> Result<Self, CMarkReadmeFromPackageError> {
25        let path = package
26            .relative_readme_path()
27            .ok_or(CMarkReadmeFromPackageError::NotFound)?;
28        let file = Arc::new(File::from_path(path.to_path_buf(), Some(package.path()))?);
29        let package_path = package.path();
30        let manifest = package.manifest();
31        Ok(Self::from_file_and_package_path_and_manifest(
32            file,
33            package_path,
34            manifest,
35        ))
36    }
37}
38
39impl CMarkReadme<(), ()> {
40    /// Creates readme from file.
41    pub fn from_file(file: Arc<File>) -> Self {
42        Self::from_file_and_package_path_and_manifest(file, (), ())
43    }
44}
45
46impl<'a, P, M> CMarkReadme<P, M> {
47    /// Adding the specified package path to the readme.
48    pub fn with_package_path(self, package_path: &'a Package) -> CMarkReadme<&'a Package, M> {
49        CMarkReadme {
50            data: self.data,
51            package_path,
52            manifest: self.manifest,
53        }
54    }
55
56    /// Adding the specified manifest to the readme.
57    pub fn with_manifest(self, manifest: &'a Manifest) -> CMarkReadme<P, &'a Manifest> {
58        CMarkReadme {
59            data: self.data,
60            package_path: self.package_path,
61            manifest,
62        }
63    }
64
65    /// Creates readme from file, package path and manifest.
66    pub fn from_file_and_package_path_and_manifest(
67        file: Arc<File>,
68        package_path: P,
69        manifest: M,
70    ) -> Self {
71        let data = CMarkData::from_file(file);
72        Self::from_data_and_package_path_and_manifest(data, package_path, manifest)
73    }
74
75    /// Creates readme from CMark items, package path and manifest.
76    pub fn from_data_and_package_path_and_manifest(
77        data: CMarkData,
78        package_path: P,
79        manifest: M,
80    ) -> Self {
81        Self {
82            data,
83            package_path,
84            manifest,
85        }
86    }
87
88    /// Returns CMark items.
89    pub fn data(&self) -> &CMarkData {
90        &self.data
91    }
92
93    /// Consumes the `CMarkReadme`, returning `CMarkData`.
94    pub fn into_data(self) -> CMarkData {
95        self.data
96    }
97
98    /// Returns the package path.
99    pub fn package_path(&self) -> &P {
100        &self.package_path
101    }
102
103    /// Returns the manifest.
104    pub fn manifest(&self) -> &M {
105        &self.manifest
106    }
107
108    /// Iterate over `CMarkItem`s.
109    pub fn iter(&self) -> CMarkDataIter<'_> {
110        self.data.iter()
111    }
112
113    /// Iterate over pulldown-cmark events.
114    pub fn iter_events(&self) -> impl Iterator<Item = &Event<'_>> {
115        self.data.iter().filter_map(|item| item.event())
116    }
117
118    fn map<F>(mut self, func: F) -> CMarkReadme<P, M>
119    where
120        F: FnOnce(CMarkData) -> CMarkData,
121    {
122        self.data = func(self.data);
123        self
124    }
125
126    fn map_result<F, E>(mut self, func: F) -> Result<CMarkReadme<P, M>, E>
127    where
128        F: FnOnce(CMarkData) -> Result<CMarkData, E>,
129    {
130        self.data = func(self.data)?;
131        Ok(self)
132    }
133
134    /// Concatenate adjacent text events.
135    ///
136    /// Use this transformation if you deleted some nodes manually
137    /// and want to merge the neighboring text nodes.
138    ///
139    /// This transformation is always applied right after
140    /// readme and docs parsing, because some text events remain ununited.
141    /// For example Rust attribute parser generate seperate text events
142    /// for every line of source code, and pulldown_cmark generate
143    /// seperate text events for character entity reference.
144    pub fn concat_texts(self) -> CMarkReadme<P, M> {
145        self.map(|data| data.concat_texts())
146    }
147
148    /// Removes first paragraph that contains only images and image-links,
149    /// if the specified predicate returns true when passing image urls to it.
150    pub fn remove_images_only_paragraph<F>(self, predicate: F) -> CMarkReadme<P, M>
151    where
152        F: FnMut(&[&str]) -> bool,
153    {
154        self.map(|data| data.remove_images_only_paragraph(predicate))
155    }
156
157    /// Removes first paragraph that contains only badges.
158    pub fn remove_badges_paragraph(self) -> CMarkReadme<P, M> {
159        self.map(|data| data.remove_badges_paragraph())
160    }
161
162    /// Remove section with the specified heading text and level and its subsections.
163    pub fn remove_section(self, heading: &str, level: u32) -> Self {
164        self.map(|data| data.remove_section(heading, level))
165    }
166
167    /// Remove the specified fenced code block tag.
168    pub fn remove_codeblock_tag(self, tag: &str) -> CMarkReadme<P, M> {
169        self.map(|data| data.remove_codeblock_tag(tag))
170    }
171
172    /// Remove the specified fenced code block tags.
173    pub fn remove_codeblock_tags(self, tags: &[&str]) -> CMarkReadme<P, M> {
174        self.map(|data| data.remove_codeblock_tags(tags))
175    }
176
177    /// Remove sections with heading `Documentation` and level 2.
178    pub fn remove_documentation_section(self) -> Self {
179        self.map(|data| data.remove_documentation_section())
180    }
181
182    /// Returns self if absolute blob links to the specified repository not found,
183    /// otherwise returns an error.
184    pub fn disallow_absolute_blob_links(
185        self,
186        repository_url: &str,
187    ) -> Result<CMarkReadme<P, M>, DisallowUrlsWithPrefixError> {
188        self.map_result(|data| data.disallow_absolute_blob_links(repository_url))
189    }
190
191    /// Convert all relative links into absolute ones using
192    /// the repository url as the root address.
193    pub fn use_absolute_blob_urls(self, repository_url: &str) -> CMarkReadme<P, M> {
194        self.map(|data| data.use_absolute_blob_urls(repository_url))
195    }
196}
197
198impl<'a, P> CMarkReadme<P, &'a Manifest> {
199    /// Returns self if absolute blob links to the manifest repository not found,
200    /// otherwise returns an error.
201    pub fn disallow_absolute_repository_blob_links(
202        self,
203    ) -> Result<CMarkReadme<P, &'a Manifest>, DisallowAbsoluteRepositoryBlobLinksError> {
204        let repository = self
205            .manifest
206            .package
207            .repository
208            .clone()
209            .ok_or(DisallowAbsoluteRepositoryBlobLinksError::DocsUrlNotFound)?;
210        Ok(self.disallow_absolute_blob_links(&repository)?)
211    }
212
213    /// Convert all relative links into absolute ones
214    /// using the manifest repository url as the root address.
215    pub fn use_absolute_repository_blob_urls(
216        self,
217    ) -> Result<CMarkReadme<P, &'a Manifest>, UseAbsoluteRepositoryBlobUrlsError> {
218        let repository = self
219            .manifest
220            .package
221            .repository
222            .clone()
223            .ok_or(UseAbsoluteRepositoryBlobUrlsError::DocsUrlNotFound)?;
224        Ok(self.use_absolute_blob_urls(&repository))
225    }
226}
227
228/// An error which can occur when creating readme from package.
229#[derive(Debug, Error)]
230pub enum CMarkReadmeFromPackageError {
231    /// File reading failed.
232    #[error(transparent)]
233    FileError(#[from] FileFromPathError),
234    /// Readme file not found.
235    #[error("CMarkReadme not found.")]
236    NotFound,
237}
238
239/// An error which can occur when checking for disallowed repository blob links.
240#[derive(Clone, Debug, Error)]
241pub enum DisallowAbsoluteRepositoryBlobLinksError {
242    /// A disallowed prefix found.
243    #[error(transparent)]
244    DisallowUrlsWithPrefixError(#[from] DisallowUrlsWithPrefixError),
245    /// Manifest does not contain `package.documentation` field.
246    #[error("Manifest does not contain package.documentation field")]
247    DocsUrlNotFound,
248}
249
250/// An error which can occur when converting relative links into absolute ones,
251/// using the manifest repository url as the root address.
252#[derive(Clone, Copy, Debug, Error)]
253pub enum UseAbsoluteRepositoryBlobUrlsError {
254    #[error("Manifest does not contain package.documentation field")]
255    DocsUrlNotFound,
256}