use crate::changelog::{Changelog, ChangelogEntry, ChangelogSection};
use crate::config::ChangelogConfig;
#[derive(Debug)]
pub struct CustomTemplateFormatter<'a> {
config: &'a ChangelogConfig,
}
impl<'a> CustomTemplateFormatter<'a> {
#[must_use]
pub fn new(config: &'a ChangelogConfig) -> Self {
Self { config }
}
#[must_use]
pub fn format(&self, changelog: &Changelog) -> String {
let mut output = String::new();
output.push_str(&self.format_version_header(changelog));
output.push_str("\n\n");
for section in &changelog.sections {
if !section.is_empty() {
output.push_str(&self.format_section(section));
output.push('\n');
}
}
output
}
pub(crate) fn format_version_header(&self, changelog: &Changelog) -> String {
let date_str = changelog.date.format("%Y-%m-%d").to_string();
let package_name = changelog.package_name.as_deref().unwrap_or("");
self.config
.template
.version_header
.replace("{version}", &changelog.version)
.replace("{date}", &date_str)
.replace("{package}", package_name)
}
pub(crate) fn format_section(&self, section: &ChangelogSection) -> String {
if section.is_empty() {
return String::new();
}
let mut output = String::new();
output.push_str(&self.format_section_header(section));
output.push_str("\n\n");
for entry in §ion.entries {
output.push_str(&self.format_entry(entry));
output.push('\n');
}
output.push('\n');
output
}
pub(crate) fn format_section_header(&self, section: &ChangelogSection) -> String {
let title = section.title();
self.config.template.section_header.replace("{title}", title).replace("{section}", title)
}
pub(crate) fn format_entry(&self, entry: &ChangelogEntry) -> String {
let date_str = entry.date.format("%Y-%m-%d").to_string();
let commit_type = entry.commit_type.as_deref().unwrap_or("");
let scope = entry.scope.as_deref().unwrap_or("");
let breaking_marker = if entry.breaking { "BREAKING: " } else { "" };
let references = if entry.references.is_empty() {
String::new()
} else if self.config.include_issue_links {
self.format_issue_links(&entry.references)
} else {
entry.references.join(", ")
};
let author = if self.config.include_authors { entry.author.clone() } else { String::new() };
let hash = if self.config.include_commit_links {
self.format_commit_link(&entry.commit_hash, &entry.short_hash)
} else {
entry.short_hash.clone()
};
let short_hash = if self.config.include_commit_links {
self.format_commit_link(&entry.commit_hash, &entry.short_hash)
} else {
entry.short_hash.clone()
};
self.config
.template
.entry_format
.replace("{description}", &entry.description)
.replace("{hash}", &hash)
.replace("{short_hash}", &short_hash)
.replace("{author}", &author)
.replace("{type}", commit_type)
.replace("{scope}", scope)
.replace("{date}", &date_str)
.replace("{references}", &references)
.replace("{breaking}", breaking_marker)
}
pub(crate) fn format_commit_link(&self, full_hash: &str, short_hash: &str) -> String {
if let Some(repo_url) = &self.config.repository_url {
let commit_url = format!("{}/commit/{}", repo_url.trim_end_matches('/'), full_hash);
format!("[{}]({})", short_hash, commit_url)
} else {
short_hash.to_string()
}
}
pub(crate) fn format_issue_links(&self, references: &[String]) -> String {
if let Some(repo_url) = &self.config.repository_url {
references
.iter()
.map(|ref_str| {
if let Some(issue_num) = ref_str.strip_prefix('#') {
let issue_url =
format!("{}/issues/{}", repo_url.trim_end_matches('/'), issue_num);
format!("[{}]({})", ref_str, issue_url)
} else {
ref_str.clone()
}
})
.collect::<Vec<_>>()
.join(" ")
} else {
references.join(" ")
}
}
#[must_use]
pub fn format_complete(&self, changelog: &Changelog) -> String {
let mut output = String::new();
if !self.config.template.header.is_empty() {
output.push_str(&self.config.template.header);
if !self.config.template.header.ends_with('\n') {
output.push('\n');
}
output.push('\n');
}
output.push_str(&self.format(changelog));
output
}
#[must_use]
pub fn format_header(&self) -> String {
self.config.template.header.clone()
}
}