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 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 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 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 Event::SoftBreak => accumulator.push_str("\n"),
135 Event::End(Tag::Paragraph) => accumulator.push_str("\n\n"),
136
137 Event::Code(text) => accumulator.push_str(&format!("`{}`", text)),
139
140 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 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}