1use chumsky::prelude::*;
5
6pub mod config;
7pub mod ignore;
8
9pub type DelimitedBy = [char; 2];
10
11#[doc(alias("Type", "Verb"))]
12pub type Keyword<'i> = &'i str;
13
14#[doc(alias("Importance", "BreakingChange"))]
15pub type Modifier<'i> = &'i str;
16
17#[doc(alias("Scope"))]
18pub type Enclosure<'i> = (&'i str, DelimitedBy);
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct Prefix<'i> {
22 pub keyword: Keyword<'i>,
23 pub modifier: Option<Modifier<'i>>,
24 pub enclosures: Vec<Enclosure<'i>>,
25}
26
27#[doc(alias("Subject"))]
28pub type Description<'i> = &'i str;
29
30#[derive(Debug, Clone, PartialEq)]
31pub struct Header<'i> {
32 pub prefix: Prefix<'i>,
33 pub description: Description<'i>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
37#[derive(serde::Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Sequence {
40 Pre,
41 Post,
42}
43
44pub type KeywordToken<'i> = Keyword<'i>;
45
46pub type ModifierToken<'i> = Modifier<'i>;
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum EnclosureToken<'i> {
50 Flexible(DelimitedBy),
51 Strict(DelimitedBy, Vec<&'i str>),
52}
53
54impl<'i> EnclosureToken<'i> {
55 #[inline]
56 pub fn delimiters(&self) -> DelimitedBy {
57 match self {
58 EnclosureToken::Flexible(delimiters) => *delimiters,
59 EnclosureToken::Strict(delimiters, _) => *delimiters,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub struct Tokens<'i> {
66 pub keywords: Vec<KeywordToken<'i>>,
67 pub modifiers: Vec<ModifierToken<'i>>,
68 pub enclosures: Vec<EnclosureToken<'i>>,
69 pub separator: char,
70
71 pub modifier_sequence: Sequence,
72}
73
74pub struct Positional {
75 pub modifier_sequence: Sequence,
76}
77
78impl Tokens<'_> {
79 pub fn preset_standard() -> Self {
80 Self {
81 keywords: vec!["add", "rem", "ref", "fix", "undo", "release"],
82 modifiers: vec!["?", "!", "!!"],
83 enclosures: vec![
84 EnclosureToken::Strict(
85 ['(', ')'],
86 vec!["exe", "lib", "test", "build", "doc", "ci", "cd"],
87 ),
88 EnclosureToken::Strict(
89 ['[', ']'],
90 vec![
91 "int", "pre", "eff", "rel", "cmp", "mnt", "tmp", "exp",
92 "sec", "upg", "ux", "pol", "sty",
93 ],
94 ),
95 ],
96 separator: ':',
97 modifier_sequence: Sequence::Pre,
98 }
99 }
100}
101
102impl Default for Tokens<'_> {
103 fn default() -> Self {
104 Self::preset_standard()
105 }
106}
107
108pub type ExtraError<'i> = Rich<'i, char>;
109
110pub type ExtraState<'i> = ();
111
112#[doc(alias("Config", "Settings"))]
113#[derive(Debug, Clone, PartialEq)]
114pub struct ExtraContext<'i> {
115 pub tokens: Tokens<'i>,
116}
117
118impl<'i> ExtraContext<'i> {
119 pub fn new(tokens: &Tokens<'i>) -> Self {
120 fn sort(v: &mut Vec<&str>) {
121 v.sort_unstable_by(|a, b| b.len().cmp(&a.len()).then(a.cmp(b)));
122 }
123
124 let mut tokens = tokens.clone();
125
126 sort(&mut tokens.keywords);
127 sort(&mut tokens.modifiers);
128
129 Self { tokens }
130 }
131}
132
133impl<'i> Default for ExtraContext<'i> {
134 fn default() -> Self {
135 Self::new(&Tokens::default())
136 }
137}
138
139impl<'i> From<Tokens<'i>> for ExtraContext<'i> {
140 fn from(val: Tokens<'i>) -> Self {
141 ExtraContext::new(&val)
142 }
143}
144
145#[doc(alias("Config", "Settings"))]
146pub type Extra<'i> =
147 extra::Full<ExtraError<'i>, ExtraState<'i>, ExtraContext<'i>>;
148
149fn ident<'i>(
150 i: &mut chumsky::input::InputRef<'i, '_, &'i str, Extra<'i>>,
151) -> (&'i str, SimpleSpan) {
152 let before = i.cursor();
153
154 while i
155 .peek()
156 .is_some_and(|c: char| c.is_alphanumeric() || c == '_')
157 {
158 i.next();
159 }
160
161 (i.slice_since(&before..), i.span_since(&before))
162}
163
164fn expected_one_of(found: &str, kind: &str, expected: &[&str]) -> String {
165 let expected = expected.join(", ");
166
167 if found.is_empty() {
168 format!("expected {kind}, one of: {expected}")
169 } else {
170 format!("unknown {kind} `{found}`, expected one of: {expected}")
171 }
172}
173
174pub fn keyword<'i>() -> impl Parser<'i, &'i str, Keyword<'i>, Extra<'i>> {
175 use chumsky::input::InputRef;
176
177 custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
178 let (s, span) = ident(i);
179 let keywords = &i.ctx().tokens.keywords;
180
181 if keywords.contains(&s) {
182 return Ok(s);
183 }
184
185 let message = expected_one_of(s, "keyword", keywords);
186
187 Err(Rich::custom(span, message))
188 })
189}
190
191pub fn modifier<'i>() -> impl Parser<'i, &'i str, Modifier<'i>, Extra<'i>> {
192 use chumsky::input::InputRef;
193
194 custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
195 let parsers = i
196 .ctx()
197 .tokens
198 .modifiers
199 .iter()
200 .map(|&token| just(token))
201 .collect::<Vec<_>>();
202
203 i.parse(choice(parsers))
204 })
205}
206
207pub fn enclosures<'i>()
208-> impl Parser<'i, &'i str, Vec<Enclosure<'i>>, Extra<'i>> {
209 use chumsky::input::InputRef;
210
211 fn parser<'i>(
212 token: &EnclosureToken<'i>,
213 ) -> impl Parser<'i, &'i str, Enclosure<'i>, Extra<'i>> {
214 match *token {
215 EnclosureToken::Flexible([start, end]) => {
216 none_of::<'i, _, _, Extra>([start, end])
217 .repeated()
218 .to_slice()
219 .delimited_by(just(start), just(end))
220 .map(move |s| (s, [start, end]))
221 .boxed()
222 }
223 EnclosureToken::Strict([start, end], ref allowed) => {
224 let allowed = allowed.clone();
225
226 custom(move |i: &mut InputRef<&'i str, Extra<'i>>| {
227 let (s, span) = ident(i);
228
229 if allowed.contains(&s) {
230 return Ok(s);
231 }
232
233 let message = expected_one_of(s, "enclosure", &allowed);
234
235 Err(Rich::custom(span, message))
236 })
237 .delimited_by(just(start), just(end))
238 .map(move |s| (s, [start, end]))
239 .boxed()
240 }
241 }
242 }
243
244 custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
245 let ctx = i.ctx();
246 let delimiters = ctx.tokens.enclosures.clone();
247 let mut index = 0;
248 let mut results = Vec::new();
249
250 loop {
251 if index >= delimiters.len() {
252 break;
253 }
254
255 let next = i.peek();
256 let is_open = delimiters[index..]
257 .iter()
258 .any(|enclosure| Some(enclosure.delimiters()[0]) == next);
259
260 if !is_open {
261 break;
262 }
263
264 let parsers =
265 delimiters[index..].iter().map(parser).collect::<Vec<_>>();
266
267 let (content, delimited_by) = i.parse(choice(parsers))?;
268 let position = delimiters
269 .iter()
270 .position(|enclosure| enclosure.delimiters() == delimited_by)
271 .unwrap();
272 index += position + 1;
273 results.push((content, delimited_by));
274 }
275
276 Ok(results)
277 })
278}
279
280pub fn separator<'i>() -> impl Parser<'i, &'i str, char, Extra<'i>> {
281 use chumsky::input::InputRef;
282
283 custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
284 let ctx = i.ctx();
285
286 i.parse(just(ctx.tokens.separator))
287 })
288}
289
290pub fn modifier_when<'i>(
291 sequence: Sequence,
292) -> impl Parser<'i, &'i str, Option<Modifier<'i>>, Extra<'i>> {
293 use chumsky::input::InputRef;
294
295 custom(move |i: &mut InputRef<&'i str, Extra<'i>>| {
296 if i.ctx().tokens.modifier_sequence != sequence {
297 return Ok(None);
298 }
299
300 i.parse(modifier().or_not())
301 })
302}
303
304pub fn description<'i>() -> impl Parser<'i, &'i str, Description<'i>, Extra<'i>>
305{
306 use chumsky::input::InputRef;
307
308 custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
309 let before = i.cursor();
310
311 while i.peek().is_some_and(|c: char| c != '\n') {
312 i.next();
313 }
314
315 let s = i.slice_since(&before..);
316 let span = i.span_since(&before);
317
318 let Some(rest) = s.strip_prefix(' ') else {
319 let message = if s.trim().is_empty() {
320 "expected a description after the separator"
321 } else {
322 "expected a space before the description"
323 };
324
325 return Err(Rich::custom(span, message));
326 };
327
328 if rest.trim().is_empty() {
329 return Err(Rich::custom(
330 span,
331 "expected a description after the separator",
332 ));
333 }
334
335 Ok(rest.trim_end())
336 })
337}
338
339pub fn prefix<'i>() -> impl Parser<'i, &'i str, Prefix<'i>, Extra<'i>> {
340 let keyword = keyword();
341
342 let modifier_pre = modifier_when(Sequence::Pre);
343
344 let enclosures = enclosures();
345
346 let modifier_post = modifier_when(Sequence::Post);
347
348 let separator = separator();
349
350 group((keyword, modifier_pre, enclosures, modifier_post, separator)).map(
351 |(keyword, modifier_pre, enclosures, modifier_post, _)| Prefix {
352 keyword,
353 modifier: modifier_pre.or(modifier_post),
354 enclosures,
355 },
356 )
357}
358
359pub fn header<'i>() -> impl Parser<'i, &'i str, Header<'i>, Extra<'i>> {
360 group((prefix(), description())).map(|(prefix, description)| Header {
361 prefix,
362 description,
363 })
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn test_keyword() {
372 fn parser_standard<'i>()
373 -> impl Parser<'i, &'i str, Keyword<'i>, Extra<'i>> {
374 keyword().with_ctx(Tokens::preset_standard().into())
375 }
376
377 assert!(parser_standard().parse("").has_errors());
378
379 assert_eq!(parser_standard().parse("add").into_result(), Ok("add"));
380 assert_eq!(parser_standard().parse("rem").into_result(), Ok("rem"));
381 assert!(parser_standard().parse("feat").has_errors());
382 }
383
384 #[test]
385 fn test_modifier() {
386 fn parser_standard<'i>()
387 -> impl Parser<'i, &'i str, Modifier<'i>, Extra<'i>> {
388 modifier().with_ctx(Tokens::preset_standard().into())
389 }
390
391 assert!(parser_standard().parse("").has_errors());
392
393 assert_eq!(parser_standard().parse("?").into_result(), Ok("?"));
394 assert_eq!(parser_standard().parse("!!").into_result(), Ok("!!"));
395 assert!(parser_standard().parse("??").has_errors());
396 }
397
398 #[test]
399 fn test_enclosures() {
400 fn parser_standard<'i>()
401 -> impl Parser<'i, &'i str, Vec<Enclosure<'i>>, Extra<'i>> {
402 enclosures().with_ctx(Tokens::preset_standard().into())
403 }
404
405 assert_eq!(parser_standard().parse("").into_result(), Ok(vec![]));
406
407 assert_eq!(
408 parser_standard().parse("(lib)").into_result(),
409 Ok(vec![("lib", ['(', ')'])])
410 );
411 assert_eq!(
412 parser_standard().parse("[pre]").into_result(),
413 Ok(vec![("pre", ['[', ']'])])
414 );
415 assert_eq!(
416 parser_standard().parse("(exe)[int]").into_result(),
417 Ok(vec![("exe", ['(', ')']), ("int", ['[', ']'])])
418 );
419 assert!(parser_standard().parse("(").has_errors());
420 assert!(parser_standard().parse("(unsupported)").has_errors());
421 assert!(parser_standard().parse("{unsupported}").has_errors());
422 assert!(parser_standard().parse("[pre](lib)").has_errors());
423 assert!(parser_standard().parse("(exe)(lib)").has_errors());
424 }
425
426 #[test]
427 fn test_enclosures_flexible() {
428 fn parser_flexible<'i>()
429 -> impl Parser<'i, &'i str, Vec<Enclosure<'i>>, Extra<'i>> {
430 let tokens = Tokens {
431 enclosures: vec![EnclosureToken::Flexible(['(', ')'])],
432 ..Tokens::preset_standard()
433 };
434
435 enclosures().with_ctx(tokens.into())
436 }
437
438 assert_eq!(parser_flexible().parse("").into_result(), Ok(vec![]));
439
440 assert_eq!(
441 parser_flexible().parse("(anything goes)").into_result(),
442 Ok(vec![("anything goes", ['(', ')'])])
443 );
444 assert_eq!(
445 parser_flexible().parse("()").into_result(),
446 Ok(vec![("", ['(', ')'])])
447 );
448 assert!(parser_flexible().parse("(unclosed").has_errors());
449 assert!(parser_flexible().parse("(nested())").has_errors());
450 }
451
452 #[test]
453 fn test_separator() {
454 fn parser_standard<'i>() -> impl Parser<'i, &'i str, char, Extra<'i>> {
455 separator().with_ctx(Tokens::preset_standard().into())
456 }
457
458 assert!(parser_standard().parse("").has_errors());
459
460 assert_eq!(parser_standard().parse(":").into_result(), Ok(':'));
461 assert!(parser_standard().parse(";").has_errors());
462 }
463
464 #[test]
465 fn test_prefix() {
466 fn parser_standard<'i>()
467 -> impl Parser<'i, &'i str, Prefix<'i>, Extra<'i>> {
468 prefix().with_ctx(Tokens::preset_standard().into())
469 }
470
471 assert!(parser_standard().parse("").has_errors());
472
473 assert_eq!(
474 parser_standard().parse("add:").into_result(),
475 Ok(Prefix {
476 keyword: "add",
477 modifier: None,
478 enclosures: vec![]
479 })
480 );
481 assert_eq!(
482 parser_standard().parse("rem?(lib):").into_result(),
483 Ok(Prefix {
484 keyword: "rem",
485 modifier: Some("?"),
486 enclosures: vec![("lib", ['(', ')'])]
487 })
488 );
489 assert_eq!(
490 parser_standard().parse("ref!![eff]:").into_result(),
491 Ok(Prefix {
492 keyword: "ref",
493 modifier: Some("!!"),
494 enclosures: vec![("eff", ['[', ']'])]
495 })
496 );
497 assert!(parser_standard().parse("add").has_errors());
498 assert!(parser_standard().parse("feat:").has_errors());
499 assert!(parser_standard().parse("add(exe)!:").has_errors());
500 }
501
502 #[test]
503 fn test_description() {
504 fn parser_standard<'i>()
505 -> impl Parser<'i, &'i str, Description<'i>, Extra<'i>> {
506 description().with_ctx(Tokens::preset_standard().into())
507 }
508
509 assert!(parser_standard().parse("").has_errors());
510 assert!(parser_standard().parse(" ").has_errors());
511 assert!(parser_standard().parse("no space").has_errors());
512
513 assert_eq!(parser_standard().parse(" ok").into_result(), Ok("ok"));
514 assert_eq!(
515 parser_standard().parse(" trailing ").into_result(),
516 Ok("trailing")
517 );
518 }
519
520 #[test]
521 fn test_header() {
522 fn parser_standard<'i>()
523 -> impl Parser<'i, &'i str, Header<'i>, Extra<'i>> {
524 header().with_ctx(Tokens::preset_standard().into())
525 }
526
527 assert!(parser_standard().parse("").has_errors());
528 assert!(parser_standard().parse("add:").has_errors());
529 assert!(parser_standard().parse("add: ").has_errors());
530 assert!(parser_standard().parse("add:no space").has_errors());
531
532 assert_eq!(
533 parser_standard()
534 .parse("add(exe)[int]: initial")
535 .into_result(),
536 Ok(Header {
537 prefix: Prefix {
538 keyword: "add",
539 modifier: None,
540 enclosures: vec![("exe", ['(', ')']), ("int", ['[', ']'])]
541 },
542 description: "initial"
543 })
544 );
545 }
546}