clparse/
lib.rs

1use anyhow::Result;
2use changelog::{Change, Changelog, ChangelogBuilder, Release, ReleaseBuilder};
3use chrono::NaiveDate;
4use err_derive::Error;
5use pulldown_cmark::{Event, LinkType, Parser, Tag};
6use versions::Version;
7use std::fs::File;
8use std::io::prelude::*;
9use std::path::PathBuf;
10
11pub mod changelog;
12
13#[derive(Clone, Debug)]
14enum ChangelogFormat {
15    Markdown,
16    Json,
17    Yaml,
18}
19
20#[derive(Clone, Debug)]
21enum ChangelogSection {
22    None,
23    Title,
24    Description,
25    ReleaseHeader,
26    ChangesetHeader,
27    Changeset(String),
28}
29
30#[derive(Debug, Error)]
31pub enum ChangelogParserError {
32    #[error(display = "unable to determine file format from contents")]
33    UnableToDetermineFormat,
34    #[error(display = "error building release")]
35    ErrorBuildingRelease(String),
36}
37
38pub struct ChangelogParser {
39    separator: String,
40    wrap: Option<usize>,
41}
42
43impl ChangelogParser {
44    pub fn new(separator: String, wrap: Option<usize>) -> Self {
45        Self {
46            separator,
47            wrap,
48        }
49    }
50
51    pub fn parse(&self, path: PathBuf) -> Result<Changelog> {
52        let mut document = String::new();
53        File::open(path.clone())?.read_to_string(&mut document)?;
54        self.parse_buffer(document)
55    }
56
57    pub fn parse_buffer(&self, buffer: String) -> Result<Changelog> {
58        match Self::get_format_from_buffer(buffer.clone()) {
59            Ok(format) => match format {
60                ChangelogFormat::Markdown => self.parse_markdown(buffer),
61                ChangelogFormat::Json => Self::parse_json(buffer),
62                ChangelogFormat::Yaml => Self::parse_yaml(buffer),
63            },
64            _ => Err(ChangelogParserError::UnableToDetermineFormat.into()),
65        }
66    }
67
68    fn parse_markdown(&self, markdown: String) -> Result<Changelog> {
69        let parser = Parser::new(&markdown);
70
71        let mut section = ChangelogSection::None;
72
73        let mut title = String::new();
74        let mut description = String::new();
75        let mut description_links = String::new();
76        let mut releases: Vec<Release> = Vec::new();
77
78        let mut release = ReleaseBuilder::default();
79        let mut changeset: Vec<Change> = Vec::new();
80        let mut accumulator = String::new();
81        let mut link_accumulator = String::new();
82
83        for event in parser {
84            match event {
85                // Headings.
86                Event::Start(Tag::Header(1)) => section = ChangelogSection::Title,
87                Event::End(Tag::Header(1)) => section = ChangelogSection::Description,
88                Event::Start(Tag::Header(2)) => {
89                    match section {
90                        ChangelogSection::Description => {
91                            description = accumulator.clone();
92                            accumulator = String::new();
93                        }
94                        ChangelogSection::Changeset(_) | ChangelogSection::ReleaseHeader => {
95                            self.parse_release_header(&mut release, &mut accumulator);
96                            self.build_release(&mut releases, &mut release, &mut changeset)?;
97                        }
98                        _ => (),
99                    }
100
101                    section = ChangelogSection::ReleaseHeader;
102                }
103                Event::Start(Tag::Header(3)) => section = ChangelogSection::ChangesetHeader,
104
105                // Links.
106                Event::Start(Tag::Link(LinkType::Inline, _, _)) => accumulator.push_str("["),
107                Event::Start(Tag::Link(LinkType::Collapsed, _, _)) => {
108                    accumulator.push_str("[");
109                    link_accumulator = String::from("[");
110                }
111                Event::End(Tag::Link(LinkType::Inline, href, _)) => {
112                    accumulator.push_str(&format!("]({})", href));
113                }
114                Event::End(Tag::Link(LinkType::Collapsed, href, _)) => {
115                    accumulator.push_str("][]");
116                    link_accumulator.push_str(&format!("]: {}\n", href));
117                    description_links.push_str(&link_accumulator);
118                    link_accumulator = String::new();
119                }
120                Event::Start(Tag::Link(LinkType::Shortcut, href, _)) => {
121                    release.link(href.to_string());
122                }
123
124                // Items.
125                Event::End(Tag::Item) => {
126                    if let ChangelogSection::Changeset(name) = section.clone() {
127                        changeset.push(Change::new(&name, accumulator)?);
128
129                        accumulator = String::new();
130                    }
131                }
132
133                // Line breaks.
134                Event::SoftBreak => accumulator.push_str("\n"),
135                Event::End(Tag::Paragraph) => accumulator.push_str("\n\n"),
136
137                // Inline code.
138                Event::Code(text) => accumulator.push_str(&format!("`{}`", text)),
139
140                // Text formatting.
141                Event::Start(Tag::Strong) | Event::End(Tag::Strong) => accumulator.push_str("**"),
142                Event::Start(Tag::Emphasis) | Event::End(Tag::Emphasis) => {
143                    accumulator.push_str("_")
144                }
145                Event::Start(Tag::Strikethrough) | Event::End(Tag::Strikethrough) => {
146                    accumulator.push_str("~~")
147                }
148
149                // Text.
150                Event::Text(text) => match section {
151                    ChangelogSection::Title => title = text.to_string(),
152                    ChangelogSection::Description => {
153                        accumulator.push_str(&text);
154
155                        if !link_accumulator.is_empty() {
156                            link_accumulator.push_str(&text);
157                        }
158                    }
159                    ChangelogSection::ChangesetHeader => {
160                        self.parse_release_header(&mut release, &mut accumulator);
161
162                        section = ChangelogSection::Changeset(text.to_string())
163                    }
164                    ChangelogSection::Changeset(_) | ChangelogSection::ReleaseHeader => accumulator.push_str(&text),
165                    _ => (),
166                },
167                _ => (),
168            };
169        }
170
171        self.build_release(&mut releases, &mut release, &mut changeset)?;
172
173        if !description_links.is_empty() {
174            description = format!("{}{}\n", description, description_links);
175        }
176
177        let changelog = ChangelogBuilder::default()
178            .title(title)
179            .description(description)
180            .releases(releases)
181            .build()
182            .map_err(ChangelogParserError::ErrorBuildingRelease)?;
183
184        Ok(changelog)
185    }
186
187    fn parse_release_header(&self, release: &mut ReleaseBuilder, accumulator: &mut String) {
188        let delimiter = format!(" {} ", self.separator);
189        if let Some((left, right)) = accumulator.trim().split_once(&delimiter) {
190            if right.contains("YANKED") {
191                release.yanked(true);
192            }
193
194            let right = &right.replace(" [YANKED]", "");
195            if let Ok(date) = NaiveDate::parse_from_str(&right, "%Y-%m-%d") {
196                release.date(date);
197            }
198
199            if let Some(version) = Version::new(&left) {
200                release.version(version);
201            }
202        }
203
204        *accumulator = String::new();
205    }
206
207    fn build_release(&self, releases: &mut Vec<Release>, release: &mut ReleaseBuilder, changeset: &mut Vec<Change>) -> Result<()> {
208        release.changes(changeset.clone());
209        release.separator(self.separator.clone());
210        release.wrap(self.wrap);
211        releases.push(
212            release
213                .build()
214                .map_err(ChangelogParserError::ErrorBuildingRelease)?
215        );
216
217        *changeset = Vec::new();
218        *release = ReleaseBuilder::default();
219
220        Ok(())
221    }
222
223    fn parse_json(json: String) -> Result<Changelog> {
224        let changelog: Changelog = serde_json::from_str(&json)?;
225
226        Ok(changelog)
227    }
228
229    fn parse_yaml(yaml: String) -> Result<Changelog> {
230        let changelog: Changelog = serde_yaml::from_str(&yaml)?;
231
232        Ok(changelog)
233    }
234
235    fn get_format_from_buffer(buffer: String) -> Result<ChangelogFormat> {
236        let first_char = match buffer.chars().next() {
237            Some(first_char) => first_char,
238            _ => {
239                return Err(ChangelogParserError::UnableToDetermineFormat.into());
240            }
241        };
242
243        let first_line: String = buffer.chars().take_while(|&c| c != '\n').collect();
244        let mut format: Option<ChangelogFormat> = match first_char {
245            '{' => Some(ChangelogFormat::Json),
246            '#' => Some(ChangelogFormat::Markdown),
247            _ => None,
248        };
249
250        if format.is_none() && (first_line == "---" || first_line.contains("title:")) {
251            format = Some(ChangelogFormat::Yaml);
252        }
253
254        if let Some(format) = format {
255            Ok(format)
256        } else {
257            Err(ChangelogParserError::UnableToDetermineFormat.into())
258        }
259    }
260}