use std::fmt::{self, Display};
use markdown::ParseOptions;
use markdown::mdast::Node;
use super::{Change, ChangeRef, MultilineText};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Changeset {
label: String,
description: Option<String>,
changes: Vec<Change>,
}
impl Changeset {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
description: None,
changes: Vec::new(),
}
}
pub fn added() -> Self {
Self::new("Added")
}
pub fn changed() -> Self {
Self::new("Changed")
}
pub fn deprecated() -> Self {
Self::new("Deprecated")
}
pub fn removed() -> Self {
Self::new("Removed")
}
pub fn fixed() -> Self {
Self::new("Fixed")
}
pub fn security() -> Self {
Self::new("Security")
}
pub fn label(&self) -> &str {
&self.label
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn set_description(&mut self, description: impl Into<String>) -> &mut Self {
self.description = Some(description.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.set_description(description);
self
}
pub fn add_change(&mut self, change: impl Into<Change>) -> &mut Self {
self.changes.push(change.into());
self
}
pub fn with_change(mut self, change: impl Into<Change>) -> Self {
self.add_change(change);
self
}
pub fn changes(&self) -> impl Iterator<Item = &Change> {
self.changes.iter()
}
}
impl Changeset {
pub(super) fn into_nodes(self) -> Vec<Node> {
let mut nodes = Vec::new();
let heading = Node::Heading(markdown::mdast::Heading {
children: vec![Node::Text(markdown::mdast::Text {
value: self.label,
position: None,
})],
position: None,
depth: 3,
});
nodes.push(heading);
if let Some(description) = self.description {
let md = markdown::to_mdast(&description, &ParseOptions::default()).expect("markdown");
if let Node::Root(root) = md {
nodes.extend(root.children);
}
}
if !self.changes.is_empty() {
let list = Node::List(markdown::mdast::List {
children: self
.changes
.into_iter()
.map(|change| {
Node::ListItem(markdown::mdast::ListItem {
children: change.into_nodes(),
position: None,
spread: false,
checked: None,
})
})
.collect(),
position: None,
ordered: false,
start: None,
spread: false,
});
nodes.push(list);
}
nodes
}
}
impl Display for Changeset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "### {}", self.label())?;
if let Some(description) = self.description() {
if f.alternate() {
write!(f, "\n\n{description:#}")?;
} else {
write!(f, "\n\n{description}")?;
}
}
let mut changes = self.changes().peekable();
if changes.peek().is_some() {
write!(f, "\n\n")?;
while let Some(change) = changes.next() {
if f.alternate() {
write!(f, "{change:#}")?;
} else {
write!(f, "{change}")?;
}
if changes.peek().is_some() {
writeln!(f)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct ChangesetRef<'a> {
label: &'a str,
nodes: &'a [Node],
}
impl<'a> ChangesetRef<'a> {
pub fn label(&self) -> &str {
self.label
}
pub fn description(&self) -> Option<MultilineText<'_>> {
MultilineText::from_nodes(self.nodes)
}
pub fn changes(&self) -> impl Iterator<Item = ChangeRef<'a>> + use<'a> {
self.nodes
.iter()
.filter_map(|node| match node {
Node::List(list) => Some(list),
_ => None,
})
.flat_map(|list| {
list.children.iter().filter_map(|node| match node {
Node::ListItem(item) => ChangeRef::from_nodes(&item.children),
_ => None,
})
})
}
pub fn to_owned(&self) -> Changeset {
Changeset {
label: self.label().to_owned(),
description: self.description().map(|text| text.to_string()),
changes: self.changes().map(|change| change.to_owned()).collect(),
}
}
}
impl<'a> ChangesetRef<'a> {
pub(super) fn from_nodes(nodes: &'a [Node]) -> Option<Self> {
let Node::Heading(heading) = nodes.first()? else {
return None;
};
if heading.depth != 3 {
return None;
}
let label = heading.children.iter().find_map(|node| match node {
Node::Text(text) => Some(&*text.value),
_ => None,
})?;
Some(Self {
label,
nodes: &nodes[1..],
})
}
}
impl Display for ChangesetRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "### {}", self.label())?;
if let Some(description) = self.description() {
if f.alternate() {
write!(f, "\n\n{description:#}")?;
} else {
write!(f, "\n\n{description}")?;
}
}
let mut changes = self.changes().peekable();
if changes.peek().is_some() {
write!(f, "\n\n")?;
while let Some(change) = changes.next() {
if f.alternate() {
write!(f, "{change:#}")?;
} else {
write!(f, "{change}")?;
}
if changes.peek().is_some() {
writeln!(f)?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use pretty_assertions::assert_eq;
use crate::changelog::Change;
use super::Changeset;
#[test]
fn test_changeset() {
let changeset = Changeset::fixed()
.with_description("Fixed some things.")
.with_change(
Change::new("Fixed `one`")
.with_url("#1", "https://github.com/ploys/example/pull/1"),
)
.with_change(
Change::new("Fixed `two`")
.with_url("#2", "https://github.com/ploys/example/pull/2"),
);
let output = indoc! {"
### Fixed
Fixed some things.
- Fixed `one` ([#1](https://github.com/ploys/example/pull/1))
- Fixed `two` ([#2](https://github.com/ploys/example/pull/2))\
"};
assert_eq!(changeset.description(), Some("Fixed some things."));
assert_eq!(changeset.changes().count(), 2);
assert_eq!(changeset.to_string(), output);
}
}