Skip to main content

git_cliff_core/
markdown.rs

1//! Markdown post-processing for the generated changelog.
2
3use pulldown_cmark::{Options, Parser};
4use pulldown_cmark_to_cmark::cmark_with_options;
5
6use crate::error::Result;
7
8/// Normalizes a Markdown string by round-tripping it through a parser and
9/// re-emitter.
10///
11/// This tidies up formatting that is tedious to get right in a Tera template
12/// (heading styles, list markers, blank lines between blocks) without the user
13/// having to fiddle with `{%-` / `trim` everywhere. The GitHub-flavored
14/// extensions git-cliff templates commonly use (tables, strikethrough, task
15/// lists, footnotes, and the rest of GFM) are enabled so they survive the
16/// round-trip.
17///
18/// The formatter options are intentionally conservative: it doesn't reflow
19/// text or rewrite links, so template output that is already valid Markdown
20/// keeps its structure.
21pub fn format_markdown(input: &str) -> Result<String> {
22    let mut options = Options::empty();
23    options.insert(Options::ENABLE_TABLES);
24    options.insert(Options::ENABLE_STRIKETHROUGH);
25    options.insert(Options::ENABLE_TASKLISTS);
26    options.insert(Options::ENABLE_FOOTNOTES);
27    options.insert(Options::ENABLE_GFM);
28
29    let parser = Parser::new_ext(input, options);
30    let mut formatted = String::with_capacity(input.len());
31    // Keep `-` as the bullet marker to match git-cliff's default templates and
32    // avoid churning existing changelogs from `-` to `*`.
33    let format_options = pulldown_cmark_to_cmark::Options {
34        list_token: '-',
35        ..Default::default()
36    };
37    cmark_with_options(parser, &mut formatted, format_options)?;
38
39    // `cmark` doesn't emit a trailing newline, but changelogs conventionally
40    // end with one. Preserve whatever the input had at the boundary.
41    if input.ends_with('\n') && !formatted.ends_with('\n') {
42        formatted.push('\n');
43    }
44    Ok(formatted)
45}
46
47#[cfg(test)]
48mod test {
49    use super::*;
50
51    #[test]
52    fn normalizes_messy_markdown() -> Result<()> {
53        // Valid but sloppy: a setext heading, missing blank line before a
54        // heading, and runs of blank lines between blocks.
55        let input = "\
56Changelog
57=========
58
59
60## 1.0.0
61### Features
62
63- first change
64- second change
65
66
67
68some text
69";
70        let formatted = format_markdown(input)?;
71        let expected = "\
72# Changelog
73
74## 1.0.0
75
76### Features
77
78- first change
79- second change
80
81some text
82";
83        assert_eq!(expected, formatted);
84        // Formatting is idempotent: a second pass changes nothing.
85        assert_eq!(formatted, format_markdown(&formatted)?);
86        Ok(())
87    }
88
89    #[test]
90    fn keeps_clean_markdown_stable() -> Result<()> {
91        let input = "\
92# Changelog
93
94## 1.0.0
95
96### Features
97
98- add a thing
99
100### Bug Fixes
101
102- fix a thing
103";
104        assert_eq!(input, format_markdown(input)?);
105        Ok(())
106    }
107}