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