use chumsky::prelude::*;
pub mod config;
pub mod ignore;
pub type DelimitedBy = [char; 2];
#[doc(alias("Type", "Verb"))]
pub type Keyword<'i> = &'i str;
#[doc(alias("Importance", "BreakingChange"))]
pub type Modifier<'i> = &'i str;
#[doc(alias("Scope"))]
pub type Enclosure<'i> = (&'i str, DelimitedBy);
#[derive(Debug, Clone, PartialEq)]
pub struct Prefix<'i> {
pub keyword: Keyword<'i>,
pub modifier: Option<Modifier<'i>>,
pub enclosures: Vec<Enclosure<'i>>,
}
#[doc(alias("Subject"))]
pub type Description<'i> = &'i str;
#[derive(Debug, Clone, PartialEq)]
pub struct Header<'i> {
pub prefix: Prefix<'i>,
pub description: Description<'i>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Sequence {
Pre,
Post,
}
pub type KeywordToken<'i> = Keyword<'i>;
pub type ModifierToken<'i> = Modifier<'i>;
#[derive(Debug, Clone, PartialEq)]
pub enum EnclosureToken<'i> {
Flexible(DelimitedBy),
Strict(DelimitedBy, Vec<&'i str>),
}
impl<'i> EnclosureToken<'i> {
#[inline]
pub fn delimiters(&self) -> DelimitedBy {
match self {
EnclosureToken::Flexible(delimiters) => *delimiters,
EnclosureToken::Strict(delimiters, _) => *delimiters,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tokens<'i> {
pub keywords: Vec<KeywordToken<'i>>,
pub modifiers: Vec<ModifierToken<'i>>,
pub enclosures: Vec<EnclosureToken<'i>>,
pub separator: char,
pub modifier_sequence: Sequence,
}
pub struct Positional {
pub modifier_sequence: Sequence,
}
impl Tokens<'_> {
pub fn preset_standard() -> Self {
Self {
keywords: vec!["add", "rem", "ref", "fix", "undo", "release"],
modifiers: vec!["?", "!", "!!"],
enclosures: vec![
EnclosureToken::Strict(
['(', ')'],
vec!["exe", "lib", "test", "build", "doc", "ci", "cd"],
),
EnclosureToken::Strict(
['[', ']'],
vec![
"int", "pre", "eff", "rel", "cmp", "mnt", "tmp", "exp",
"sec", "upg", "ux", "pol", "sty",
],
),
],
separator: ':',
modifier_sequence: Sequence::Pre,
}
}
}
impl Default for Tokens<'_> {
fn default() -> Self {
Self::preset_standard()
}
}
pub type ExtraError<'i> = Rich<'i, char>;
pub type ExtraState<'i> = ();
#[doc(alias("Config", "Settings"))]
#[derive(Debug, Clone, PartialEq)]
pub struct ExtraContext<'i> {
pub tokens: Tokens<'i>,
}
impl<'i> ExtraContext<'i> {
pub fn new(tokens: &Tokens<'i>) -> Self {
fn sort(v: &mut Vec<&str>) {
v.sort_unstable_by(|a, b| b.len().cmp(&a.len()).then(a.cmp(b)));
}
let mut tokens = tokens.clone();
sort(&mut tokens.keywords);
sort(&mut tokens.modifiers);
Self { tokens }
}
}
impl<'i> Default for ExtraContext<'i> {
fn default() -> Self {
Self::new(&Tokens::default())
}
}
impl<'i> From<Tokens<'i>> for ExtraContext<'i> {
fn from(val: Tokens<'i>) -> Self {
ExtraContext::new(&val)
}
}
#[doc(alias("Config", "Settings"))]
pub type Extra<'i> =
extra::Full<ExtraError<'i>, ExtraState<'i>, ExtraContext<'i>>;
fn ident<'i>(
i: &mut chumsky::input::InputRef<'i, '_, &'i str, Extra<'i>>,
) -> (&'i str, SimpleSpan) {
let before = i.cursor();
while i
.peek()
.is_some_and(|c: char| c.is_alphanumeric() || c == '_')
{
i.next();
}
(i.slice_since(&before..), i.span_since(&before))
}
fn expected_one_of(found: &str, kind: &str, expected: &[&str]) -> String {
let expected = expected.join(", ");
if found.is_empty() {
format!("expected {kind}, one of: {expected}")
} else {
format!("unknown {kind} `{found}`, expected one of: {expected}")
}
}
pub fn keyword<'i>() -> impl Parser<'i, &'i str, Keyword<'i>, Extra<'i>> {
use chumsky::input::InputRef;
custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
let (s, span) = ident(i);
let keywords = &i.ctx().tokens.keywords;
if keywords.contains(&s) {
return Ok(s);
}
let message = expected_one_of(s, "keyword", keywords);
Err(Rich::custom(span, message))
})
}
pub fn modifier<'i>() -> impl Parser<'i, &'i str, Modifier<'i>, Extra<'i>> {
use chumsky::input::InputRef;
custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
let parsers = i
.ctx()
.tokens
.modifiers
.iter()
.map(|&token| just(token))
.collect::<Vec<_>>();
i.parse(choice(parsers))
})
}
pub fn enclosures<'i>()
-> impl Parser<'i, &'i str, Vec<Enclosure<'i>>, Extra<'i>> {
use chumsky::input::InputRef;
fn parser<'i>(
token: &EnclosureToken<'i>,
) -> impl Parser<'i, &'i str, Enclosure<'i>, Extra<'i>> {
match *token {
EnclosureToken::Flexible([start, end]) => {
none_of::<'i, _, _, Extra>([start, end])
.repeated()
.to_slice()
.delimited_by(just(start), just(end))
.map(move |s| (s, [start, end]))
.boxed()
}
EnclosureToken::Strict([start, end], ref allowed) => {
let allowed = allowed.clone();
custom(move |i: &mut InputRef<&'i str, Extra<'i>>| {
let (s, span) = ident(i);
if allowed.contains(&s) {
return Ok(s);
}
let message = expected_one_of(s, "enclosure", &allowed);
Err(Rich::custom(span, message))
})
.delimited_by(just(start), just(end))
.map(move |s| (s, [start, end]))
.boxed()
}
}
}
custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
let ctx = i.ctx();
let delimiters = ctx.tokens.enclosures.clone();
let mut index = 0;
let mut results = Vec::new();
loop {
if index >= delimiters.len() {
break;
}
let next = i.peek();
let is_open = delimiters[index..]
.iter()
.any(|enclosure| Some(enclosure.delimiters()[0]) == next);
if !is_open {
break;
}
let parsers =
delimiters[index..].iter().map(parser).collect::<Vec<_>>();
let (content, delimited_by) = i.parse(choice(parsers))?;
let position = delimiters
.iter()
.position(|enclosure| enclosure.delimiters() == delimited_by)
.unwrap();
index += position + 1;
results.push((content, delimited_by));
}
Ok(results)
})
}
pub fn separator<'i>() -> impl Parser<'i, &'i str, char, Extra<'i>> {
use chumsky::input::InputRef;
custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
let ctx = i.ctx();
i.parse(just(ctx.tokens.separator))
})
}
pub fn modifier_when<'i>(
sequence: Sequence,
) -> impl Parser<'i, &'i str, Option<Modifier<'i>>, Extra<'i>> {
use chumsky::input::InputRef;
custom(move |i: &mut InputRef<&'i str, Extra<'i>>| {
if i.ctx().tokens.modifier_sequence != sequence {
return Ok(None);
}
i.parse(modifier().or_not())
})
}
pub fn description<'i>() -> impl Parser<'i, &'i str, Description<'i>, Extra<'i>>
{
use chumsky::input::InputRef;
custom(|i: &mut InputRef<&'i str, Extra<'i>>| {
let before = i.cursor();
while i.peek().is_some_and(|c: char| c != '\n') {
i.next();
}
let s = i.slice_since(&before..);
let span = i.span_since(&before);
let Some(rest) = s.strip_prefix(' ') else {
let message = if s.trim().is_empty() {
"expected a description after the separator"
} else {
"expected a space before the description"
};
return Err(Rich::custom(span, message));
};
if rest.trim().is_empty() {
return Err(Rich::custom(
span,
"expected a description after the separator",
));
}
Ok(rest.trim_end())
})
}
pub fn prefix<'i>() -> impl Parser<'i, &'i str, Prefix<'i>, Extra<'i>> {
let keyword = keyword();
let modifier_pre = modifier_when(Sequence::Pre);
let enclosures = enclosures();
let modifier_post = modifier_when(Sequence::Post);
let separator = separator();
group((keyword, modifier_pre, enclosures, modifier_post, separator)).map(
|(keyword, modifier_pre, enclosures, modifier_post, _)| Prefix {
keyword,
modifier: modifier_pre.or(modifier_post),
enclosures,
},
)
}
pub fn header<'i>() -> impl Parser<'i, &'i str, Header<'i>, Extra<'i>> {
group((prefix(), description())).map(|(prefix, description)| Header {
prefix,
description,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keyword() {
fn parser_standard<'i>()
-> impl Parser<'i, &'i str, Keyword<'i>, Extra<'i>> {
keyword().with_ctx(Tokens::preset_standard().into())
}
assert!(parser_standard().parse("").has_errors());
assert_eq!(parser_standard().parse("add").into_result(), Ok("add"));
assert_eq!(parser_standard().parse("rem").into_result(), Ok("rem"));
assert!(parser_standard().parse("feat").has_errors());
}
#[test]
fn test_modifier() {
fn parser_standard<'i>()
-> impl Parser<'i, &'i str, Modifier<'i>, Extra<'i>> {
modifier().with_ctx(Tokens::preset_standard().into())
}
assert!(parser_standard().parse("").has_errors());
assert_eq!(parser_standard().parse("?").into_result(), Ok("?"));
assert_eq!(parser_standard().parse("!!").into_result(), Ok("!!"));
assert!(parser_standard().parse("??").has_errors());
}
#[test]
fn test_enclosures() {
fn parser_standard<'i>()
-> impl Parser<'i, &'i str, Vec<Enclosure<'i>>, Extra<'i>> {
enclosures().with_ctx(Tokens::preset_standard().into())
}
assert_eq!(parser_standard().parse("").into_result(), Ok(vec![]));
assert_eq!(
parser_standard().parse("(lib)").into_result(),
Ok(vec![("lib", ['(', ')'])])
);
assert_eq!(
parser_standard().parse("[pre]").into_result(),
Ok(vec![("pre", ['[', ']'])])
);
assert_eq!(
parser_standard().parse("(exe)[int]").into_result(),
Ok(vec![("exe", ['(', ')']), ("int", ['[', ']'])])
);
assert!(parser_standard().parse("(").has_errors());
assert!(parser_standard().parse("(unsupported)").has_errors());
assert!(parser_standard().parse("{unsupported}").has_errors());
assert!(parser_standard().parse("[pre](lib)").has_errors());
assert!(parser_standard().parse("(exe)(lib)").has_errors());
}
#[test]
fn test_enclosures_flexible() {
fn parser_flexible<'i>()
-> impl Parser<'i, &'i str, Vec<Enclosure<'i>>, Extra<'i>> {
let tokens = Tokens {
enclosures: vec![EnclosureToken::Flexible(['(', ')'])],
..Tokens::preset_standard()
};
enclosures().with_ctx(tokens.into())
}
assert_eq!(parser_flexible().parse("").into_result(), Ok(vec![]));
assert_eq!(
parser_flexible().parse("(anything goes)").into_result(),
Ok(vec![("anything goes", ['(', ')'])])
);
assert_eq!(
parser_flexible().parse("()").into_result(),
Ok(vec![("", ['(', ')'])])
);
assert!(parser_flexible().parse("(unclosed").has_errors());
assert!(parser_flexible().parse("(nested())").has_errors());
}
#[test]
fn test_separator() {
fn parser_standard<'i>() -> impl Parser<'i, &'i str, char, Extra<'i>> {
separator().with_ctx(Tokens::preset_standard().into())
}
assert!(parser_standard().parse("").has_errors());
assert_eq!(parser_standard().parse(":").into_result(), Ok(':'));
assert!(parser_standard().parse(";").has_errors());
}
#[test]
fn test_prefix() {
fn parser_standard<'i>()
-> impl Parser<'i, &'i str, Prefix<'i>, Extra<'i>> {
prefix().with_ctx(Tokens::preset_standard().into())
}
assert!(parser_standard().parse("").has_errors());
assert_eq!(
parser_standard().parse("add:").into_result(),
Ok(Prefix {
keyword: "add",
modifier: None,
enclosures: vec![]
})
);
assert_eq!(
parser_standard().parse("rem?(lib):").into_result(),
Ok(Prefix {
keyword: "rem",
modifier: Some("?"),
enclosures: vec![("lib", ['(', ')'])]
})
);
assert_eq!(
parser_standard().parse("ref!![eff]:").into_result(),
Ok(Prefix {
keyword: "ref",
modifier: Some("!!"),
enclosures: vec![("eff", ['[', ']'])]
})
);
assert!(parser_standard().parse("add").has_errors());
assert!(parser_standard().parse("feat:").has_errors());
assert!(parser_standard().parse("add(exe)!:").has_errors());
}
#[test]
fn test_description() {
fn parser_standard<'i>()
-> impl Parser<'i, &'i str, Description<'i>, Extra<'i>> {
description().with_ctx(Tokens::preset_standard().into())
}
assert!(parser_standard().parse("").has_errors());
assert!(parser_standard().parse(" ").has_errors());
assert!(parser_standard().parse("no space").has_errors());
assert_eq!(parser_standard().parse(" ok").into_result(), Ok("ok"));
assert_eq!(
parser_standard().parse(" trailing ").into_result(),
Ok("trailing")
);
}
#[test]
fn test_header() {
fn parser_standard<'i>()
-> impl Parser<'i, &'i str, Header<'i>, Extra<'i>> {
header().with_ctx(Tokens::preset_standard().into())
}
assert!(parser_standard().parse("").has_errors());
assert!(parser_standard().parse("add:").has_errors());
assert!(parser_standard().parse("add: ").has_errors());
assert!(parser_standard().parse("add:no space").has_errors());
assert_eq!(
parser_standard()
.parse("add(exe)[int]: initial")
.into_result(),
Ok(Header {
prefix: Prefix {
keyword: "add",
modifier: None,
enclosures: vec![("exe", ['(', ')']), ("int", ['[', ']'])]
},
description: "initial"
})
);
}
}