Adjust Tag syntax to match TaskWarrior

This commit is contained in:
Dustin J. Mitchell 2021-01-10 03:43:44 +00:00
parent 2bacc05ced
commit b95c146a7e
4 changed files with 55 additions and 22 deletions

View file

@ -10,7 +10,8 @@ use nom::{
sequence::*,
Err, IResult,
};
use taskchampion::{Status, Uuid};
use std::convert::TryFrom;
use taskchampion::{Status, Tag, Uuid};
/// A task identifier, as given in a filter command-line expression
#[derive(Debug, PartialEq, Clone)]
@ -130,7 +131,10 @@ pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
Ok(input.1)
}
map_res(
all_consuming(tuple((char('+'), recognize(pair(alpha1, alphanumeric0))))),
all_consuming(tuple((
char('+'),
recognize(verify(rest, |s: &str| Tag::try_from(s).is_ok())),
))),
to_tag,
)(input)
}
@ -141,7 +145,10 @@ pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
Ok(input.1)
}
map_res(
all_consuming(tuple((char('-'), recognize(pair(alpha1, alphanumeric0))))),
all_consuming(tuple((
char('-'),
recognize(verify(rest, |s: &str| Tag::try_from(s).is_ok())),
))),
to_tag,
)(input)
}

View file

@ -5,6 +5,7 @@
* [Using the Task Command](./using-task-command.md)
* [Configuration](./config-file.md)
* [Reports](./reports.md)
* [Tags](./tags.md)
* [Synchronization](./task-sync.md)
* [Running the Sync Server](./running-sync-server.md)
* [Debugging](./debugging.md)

12
docs/src/tags.md Normal file
View file

@ -0,0 +1,12 @@
# Tags
Each task has a collection of associated tags.
Tags are short words that categorize tasks, typically written with a leading `+`, such as `+next` or `+jobsearch`.
Tags are useful for filtering tasks in reports or on the command line.
For example, when it's time to continue the job search, `task +jobsearch` will show pending tasks with the `jobsearch` tag.
## Allowed Tags
Specifically, tags must be at least one character long and cannot contain whitespace or any of the characters `+-*/(<>^! %=~`.
The first character cannot be a digit, and `:` is not allowed after the first character.

View file

@ -84,22 +84,35 @@ impl Status {
}
/// A Tag is a newtype around a String that limits its values to valid tags.
///
/// Valid tags must not contain whitespace or any of the characters in [`INVALID_TAG_CHARACTERS`].
/// The first characters additionally cannot be a digit, and subsequent characters cannot be `:`.
/// This definition is based on [that of
/// TaskWarrior](https://github.com/GothenburgBitFactory/taskwarrior/blob/663c6575ceca5bd0135ae884879339dac89d3142/src/Lexer.cpp#L146-L164).
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct Tag(String);
pub const INVALID_TAG_CHARACTERS: &str = "+-*/(<>^! %=~";
impl Tag {
fn from_str(value: &str) -> Result<Tag, failure::Error> {
fn err(value: &str) -> Result<Tag, failure::Error> {
Err(format_err!("invalid tag {:?}", value))
}
if let Some(c) = value.chars().next() {
if !c.is_ascii_alphabetic() {
return Err(format_err!("first character of a tag must be alphabetic"));
if c.is_whitespace() || c.is_ascii_digit() || INVALID_TAG_CHARACTERS.contains(c) {
return err(value);
}
} else {
return Err(format_err!("tags must have at least one character"));
return err(value);
}
if !value.chars().skip(1).all(|c| c.is_ascii_alphanumeric()) {
return Err(format_err!(
"characters of a tag after the first must be alphanumeric"
));
if !value
.chars()
.skip(1)
.all(|c| !(c.is_whitespace() || c == ':' || INVALID_TAG_CHARACTERS.contains(c)))
{
return err(value);
}
Ok(Self(String::from(value)))
}
@ -383,23 +396,23 @@ mod test {
let tag: Tag = "abc".try_into().unwrap();
assert_eq!(tag, Tag("abc".to_owned()));
let tag: Tag = ":abc".try_into().unwrap();
assert_eq!(tag, Tag(":abc".to_owned()));
let tag: Tag = "a123_456".try_into().unwrap();
assert_eq!(tag, Tag("a123_456".to_owned()));
let tag: Result<Tag, _> = "".try_into();
assert_eq!(
tag.unwrap_err().to_string(),
"tags must have at least one character"
);
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"\"");
let tag: Result<Tag, _> = "a:b".try_into();
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"a:b\"");
let tag: Result<Tag, _> = "999".try_into();
assert_eq!(
tag.unwrap_err().to_string(),
"first character of a tag must be alphabetic"
);
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"999\"");
let tag: Result<Tag, _> = "abc!!".try_into();
assert_eq!(
tag.unwrap_err().to_string(),
"characters of a tag after the first must be alphanumeric"
);
assert_eq!(tag.unwrap_err().to_string(), "invalid tag \"abc!!\"");
}
#[test]