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