1#![allow(clippy::let_unit_value)] use std::str;
4
5use winnow::ascii::line_ending;
6use winnow::combinator::alt;
7use winnow::combinator::repeat;
8use winnow::combinator::trace;
9use winnow::combinator::{cut_err, eof, fail, opt, peek};
10use winnow::combinator::{delimited, preceded, terminated};
11use winnow::error::{AddContext, ErrMode, ParserError, StrContext};
12use winnow::prelude::*;
13use winnow::token::{take, take_till, take_while};
14
15type CommitDetails<'a> = (
16 &'a str,
17 Option<&'a str>,
18 bool,
19 &'a str,
20 Option<&'a str>,
21 Vec<(&'a str, &'a str, &'a str)>,
22);
23
24pub(crate) fn parse<
25 'a,
26 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
27>(
28 i: &mut &'a str,
29) -> ModalResult<CommitDetails<'a>, E> {
30 message.parse_next(i)
31}
32
33fn is_line_ending(c: char) -> bool {
37 c == '\n' || c == '\r'
38}
39
40fn is_parens(c: char) -> bool {
42 c == '(' || c == ')'
43}
44
45fn is_whitespace(c: char) -> bool {
56 c.is_whitespace()
57}
58
59fn whitespace<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
60 i: &mut &'a str,
61) -> ModalResult<&'a str, E> {
62 take_while(0.., is_whitespace).parse_next(i)
63}
64
65pub(crate) fn message<
69 'a,
70 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
71>(
72 i: &mut &'a str,
73) -> ModalResult<CommitDetails<'a>, E> {
74 trace("message", move |i: &mut &'a str| {
75 let summary =
76 terminated(trace("summary", summary), alt((line_ending, eof))).parse_next(i)?;
77 let (type_, scope, breaking, description) = summary;
78
79 let _ = alt((line_ending, eof))
81 .context(StrContext::Label(BODY))
82 .parse_next(i)?;
83
84 let _extra: () = repeat(0.., line_ending).parse_next(i)?;
85
86 let body = opt(body).parse_next(i)?;
87
88 let footers = repeat(0.., footer).parse_next(i)?;
89
90 let _: () = repeat(0.., line_ending).parse_next(i)?;
91
92 Ok((type_, scope, breaking.is_some(), description, body, footers))
93 })
94 .parse_next(i)
95}
96
97pub(crate) fn type_<
99 'a,
100 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
101>(
102 i: &mut &'a str,
103) -> ModalResult<&'a str, E> {
104 trace(
105 "type",
106 take_while(1.., |c: char| {
107 !is_line_ending(c) && !is_parens(c) && c != ':' && c != '!' && !is_whitespace(c)
108 })
109 .context(StrContext::Label(TYPE)),
110 )
111 .parse_next(i)
112}
113
114pub(crate) const TYPE: &str = "type";
115
116pub(crate) fn scope<
118 'a,
119 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
120>(
121 i: &mut &'a str,
122) -> ModalResult<&'a str, E> {
123 trace(
124 "scope",
125 take_while(1.., |c: char| !is_line_ending(c) && !is_parens(c))
126 .context(StrContext::Label(SCOPE)),
127 )
128 .parse_next(i)
129}
130
131pub(crate) const SCOPE: &str = "scope";
132
133#[allow(clippy::type_complexity)]
137fn summary<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
138 i: &mut &'a str,
139) -> ModalResult<(&'a str, Option<&'a str>, Option<&'a str>, &'a str), E> {
140 trace(
141 "summary",
142 (
143 type_,
144 opt(delimited('(', cut_err(scope), ')')),
145 opt(exclamation_mark),
146 preceded(
147 (':', whitespace),
148 text.context(StrContext::Label(DESCRIPTION)),
149 ),
150 ),
151 )
152 .context(StrContext::Label(SUMMARY))
153 .parse_next(i)
154}
155
156pub(crate) const SUMMARY: &str = "SUMMARY";
157pub(crate) const DESCRIPTION: &str = "description";
158
159fn text<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
161 i: &mut &'a str,
162) -> ModalResult<&'a str, E> {
163 trace("text", take_till(1.., is_line_ending)).parse_next(i)
164}
165
166fn body<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
167 i: &mut &'a str,
168) -> ModalResult<&'a str, E> {
169 trace("body", move |i: &mut &'a str| {
170 if i.is_empty() {
171 let start = i.checkpoint();
172 let err = E::from_input(i);
173 let err = err.add_context(i, &start, StrContext::Label(BODY));
174 return Err(ErrMode::Backtrack(err));
175 }
176
177 let mut offset = 0;
178 let mut prior_is_empty = true;
179 for line in crate::lines::LinesWithTerminator::new(i) {
180 if prior_is_empty
181 && peek::<_, _, ErrMode<E>, _>((token, separator))
182 .parse_peek(line.trim_end())
183 .is_ok()
184 {
185 break;
186 }
187 prior_is_empty = line.trim().is_empty();
188
189 offset += line.chars().count();
190 }
191 if offset == 0 {
192 fail::<_, (), _>(i)?;
193 }
194
195 take(offset).map(str::trim_end).parse_next(i)
196 })
197 .parse_next(i)
198}
199
200pub(crate) const BODY: &str = "body";
201
202fn footer<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
204 i: &mut &'a str,
205) -> ModalResult<(&'a str, &'a str, &'a str), E> {
206 trace(
207 "footer",
208 (token, separator, whitespace, value).map(|(ft, s, _, fv)| (ft, s, fv)),
209 )
210 .parse_next(i)
211}
212
213pub(crate) fn token<
216 'a,
217 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
218>(
219 i: &mut &'a str,
220) -> ModalResult<&'a str, E> {
221 trace("token", alt(("BREAKING CHANGE", type_))).parse_next(i)
222}
223
224fn separator<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
226 i: &mut &'a str,
227) -> ModalResult<&'a str, E> {
228 trace("sep", alt((":", " #"))).parse_next(i)
229}
230
231pub(crate) fn value<
232 'a,
233 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
234>(
235 i: &mut &'a str,
236) -> ModalResult<&'a str, E> {
237 if i.is_empty() {
238 let start = i.checkpoint();
239 let err = E::from_input(i);
240 let err = err.add_context(i, &start, StrContext::Label("value"));
241 return Err(ErrMode::Cut(err));
242 }
243
244 let mut offset = 0;
245 for (i, line) in crate::lines::LinesWithTerminator::new(i).enumerate() {
246 if 0 < i
247 && peek::<_, _, ErrMode<E>, _>((token, separator))
248 .parse_peek(line.trim_end())
249 .is_ok()
250 {
251 break;
252 }
253
254 offset += line.chars().count();
255 }
256
257 take(offset).map(str::trim_end).parse_next(i)
258}
259
260fn exclamation_mark<
261 'a,
262 E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
263>(
264 i: &mut &'a str,
265) -> ModalResult<&'a str, E> {
266 "!".context(StrContext::Label(BREAKER)).parse_next(i)
267}
268
269pub(crate) const BREAKER: &str = "exclamation_mark";
270
271#[cfg(test)]
272#[allow(clippy::non_ascii_literal)]
273mod tests {
274 use super::*;
275
276 use winnow::error::ContextError;
277
278 mod message {
279 use super::*;
280 #[test]
281 fn errors() {
282 let mut p = message::<ContextError>;
283
284 let input = "Hello World";
285 let err = p.parse(input).unwrap_err();
286 let err = crate::Error::with_nom(input, err);
287 assert_eq!(err.to_string(), crate::ErrorKind::MissingType.to_string());
288
289 let input = "fix Improved error messages\n";
290 let err = p.parse(input).unwrap_err();
291 let err = crate::Error::with_nom(input, err);
292 assert_eq!(err.to_string(), crate::ErrorKind::MissingType.to_string());
293 }
294 }
295
296 mod summary {
297 use super::*;
298
299 #[test]
300 fn test_type() {
301 let mut p = type_::<ContextError>;
302
303 assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
305 assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
306 assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
307 assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
308 assert_eq!(p.parse_peek("foo2bar").unwrap(), ("", "foo2bar"));
309 assert_eq!(p.parse_peek("foo-bar").unwrap(), ("", "foo-bar"));
310 assert_eq!(p.parse_peek("foo bar").unwrap(), (" bar", "foo"));
311 assert_eq!(p.parse_peek("foo: bar").unwrap(), (": bar", "foo"));
312 assert_eq!(p.parse_peek("foo!: bar").unwrap(), ("!: bar", "foo"));
313 assert_eq!(p.parse_peek("foo(bar").unwrap(), ("(bar", "foo"));
314 assert_eq!(p.parse_peek("foo ").unwrap(), (" ", "foo"));
315
316 assert!(p.parse_peek("").is_err());
318 assert!(p.parse_peek(" ").is_err());
319 assert!(p.parse_peek(" ").is_err());
320 assert!(p.parse_peek(")").is_err());
321 assert!(p.parse_peek(" feat").is_err());
322 assert!(p.parse_peek(" feat ").is_err());
323 }
324
325 #[test]
326 fn test_scope() {
327 let mut p = scope::<ContextError>;
328
329 assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
331 assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
332 assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
333 assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
334 assert_eq!(p.parse_peek("foo bar").unwrap(), ("", "foo bar"));
335 assert_eq!(p.parse_peek("foo-bar").unwrap(), ("", "foo-bar"));
336 assert_eq!(p.parse_peek("x86").unwrap(), ("", "x86"));
337
338 assert!(p.parse_peek("").is_err());
340 assert!(p.parse_peek(")").is_err());
341 }
342
343 #[test]
344 fn test_text() {
345 let mut p = text::<ContextError>;
346
347 assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
349 assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
350 assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
351 assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
352 assert_eq!(p.parse_peek("foo bar").unwrap(), ("", "foo bar"));
353 assert_eq!(p.parse_peek("foo bar\n").unwrap(), ("\n", "foo bar"));
354 assert_eq!(
355 p.parse_peek("foo\nbar\nbaz").unwrap(),
356 ("\nbar\nbaz", "foo")
357 );
358
359 assert!(p.parse_peek("").is_err());
361 }
362
363 #[test]
364 fn test_summary() {
365 let mut p = summary::<ContextError>;
366
367 assert_eq!(
369 p.parse_peek("foo: bar").unwrap(),
370 ("", ("foo", None, None, "bar"))
371 );
372 assert_eq!(
373 p.parse_peek("foo(bar): baz").unwrap(),
374 ("", ("foo", Some("bar"), None, "baz"))
375 );
376 assert_eq!(
377 p.parse_peek("foo(bar): baz").unwrap(),
378 ("", ("foo", Some("bar"), None, "baz"))
379 );
380 assert_eq!(
381 p.parse_peek("foo(bar-baz): qux").unwrap(),
382 ("", ("foo", Some("bar-baz"), None, "qux"))
383 );
384 assert_eq!(
385 p.parse_peek("foo!: bar").unwrap(),
386 ("", ("foo", None, Some("!"), "bar"))
387 );
388
389 assert!(p.parse_peek("").is_err());
391 assert!(p.parse_peek(" ").is_err());
392 assert!(p.parse_peek(" ").is_err());
393 assert!(p.parse_peek("foo").is_err());
394 assert!(p.parse_peek("foo bar").is_err());
395 assert!(p.parse_peek("foo : bar").is_err());
396 assert!(p.parse_peek("foo bar: baz").is_err());
397 assert!(p.parse_peek("foo(: bar").is_err());
398 assert!(p.parse_peek("foo): bar").is_err());
399 assert!(p.parse_peek("foo(): bar").is_err());
400 assert!(p.parse_peek("foo(bar)").is_err());
401 assert!(p.parse_peek("foo(bar):").is_err());
402 assert!(p.parse_peek("foo(bar): ").is_err());
403 assert!(p.parse_peek("foo(bar): ").is_err());
404 assert!(p.parse_peek("foo(bar) :baz").is_err());
405 assert!(p.parse_peek("foo(bar) : baz").is_err());
406 assert!(p.parse_peek("foo (bar): baz").is_err());
407 assert!(p.parse_peek("foo bar(baz): qux").is_err());
408 }
409 }
410
411 mod body {
412 use super::*;
413
414 #[test]
415 fn test_body() {
416 let mut p = body::<ContextError>;
417
418 assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
420 assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
421 assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
422 assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
423 assert_eq!(
424 p.parse_peek(" code block").unwrap(),
425 ("", " code block")
426 );
427 assert_eq!(p.parse_peek("💃🏽").unwrap(), ("", "💃🏽"));
428 assert_eq!(p.parse_peek("foo bar").unwrap(), ("", "foo bar"));
429 assert_eq!(
430 p.parse_peek("foo\nbar\n\nbaz").unwrap(),
431 ("", "foo\nbar\n\nbaz")
432 );
433 assert_eq!(
434 p.parse_peek("foo\n\nBREAKING CHANGE: oops!").unwrap(),
435 ("BREAKING CHANGE: oops!", "foo")
436 );
437 assert_eq!(
438 p.parse_peek("foo\n\nBREAKING-CHANGE: bar").unwrap(),
439 ("BREAKING-CHANGE: bar", "foo")
440 );
441 assert_eq!(
442 p.parse_peek("foo\n\nMy-Footer: bar").unwrap(),
443 ("My-Footer: bar", "foo")
444 );
445 assert_eq!(
446 p.parse_peek("foo\n\nMy-Footer #bar").unwrap(),
447 ("My-Footer #bar", "foo")
448 );
449
450 assert!(p.parse_peek("").is_err());
452 }
453
454 #[test]
455 fn test_footer() {
456 let mut p = footer::<ContextError>;
457
458 assert_eq!(
460 p.parse_peek("hello: world").unwrap(),
461 ("", ("hello", ":", "world"))
462 );
463 assert_eq!(
464 p.parse_peek("BREAKING CHANGE: woops!").unwrap(),
465 ("", ("BREAKING CHANGE", ":", "woops!"))
466 );
467 assert_eq!(
468 p.parse_peek("Co-Authored-By: Marge Simpson <marge@simpsons.com>")
469 .unwrap(),
470 (
471 "",
472 ("Co-Authored-By", ":", "Marge Simpson <marge@simpsons.com>")
473 )
474 );
475 assert_eq!(
476 p.parse_peek("Closes #12").unwrap(),
477 ("", ("Closes", " #", "12"))
478 );
479 assert_eq!(
480 p.parse_peek("BREAKING-CHANGE: broken").unwrap(),
481 ("", ("BREAKING-CHANGE", ":", "broken"))
482 );
483
484 assert!(p.parse_peek("").is_err());
486 assert!(p.parse_peek(" ").is_err());
487 assert!(p.parse_peek(" ").is_err());
488 assert!(p.parse_peek("foo").is_err());
489 assert!(p.parse_peek("foo:").is_err());
490 assert!(p.parse_peek("foo: ").is_err());
491 assert!(p.parse_peek("foo ").is_err());
492 assert!(p.parse_peek("foo #").is_err());
493 assert!(p.parse_peek("BREAKING CHANGE").is_err());
494 assert!(p.parse_peek("BREAKING CHANGE:").is_err());
495 assert!(p.parse_peek("Foo-Bar").is_err());
496 assert!(p.parse_peek("Foo-Bar: ").is_err());
497 assert!(p.parse_peek("foo").is_err());
498 }
499 }
500}