diff --git a/trinitry/.gitignore b/trinitry/.gitignore new file mode 100644 index 0000000..4424904 --- /dev/null +++ b/trinitry/.gitignore @@ -0,0 +1,6 @@ +# build +/target +/result + +# It is a library +Cargo.lock diff --git a/trinitry/Cargo.toml b/trinitry/Cargo.toml new file mode 100644 index 0000000..7856ba1 --- /dev/null +++ b/trinitry/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "trinitry" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pest = "2.7.5" +pest_derive = {version = "2.7.5", features = ["grammar-extras"]} +thiserror = "1.0.50" + +# The header imports katex, a js latex parser, into the doc comments +[package.metadata.docs.rs] +rustdoc-args = [ "--html-in-header", "./docs-header.html" ] diff --git a/trinitry/docs-header.html b/trinitry/docs-header.html new file mode 100644 index 0000000..beafd4d --- /dev/null +++ b/trinitry/docs-header.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/trinitry/src/lib.rs b/trinitry/src/lib.rs new file mode 100644 index 0000000..2af4dd3 --- /dev/null +++ b/trinitry/src/lib.rs @@ -0,0 +1,192 @@ +//! This crate is a parser for the 'Trinitry' (not 'Trinity') language, used to map all sort of +//! Functions to a memorable command. +//! +//! This parser is more of a validator, as Trinitry does not support any language features besides +//! the aforementioned commands and arguments. That includes some simple constructs like: '||' (OR) +//! or '&&' (AND). If you need these features, simple write them in the language, you've written your +//! Function in. +//! +//! # General specification +//! ## Command +//! Basically every command can be a series of alphanumeric ASCII values. +//! +//! Correctly spoken, the Language, containing all valid command names, is just the Kleene closure +//! over an Alphabet $\Sigma$, which contains all alphanumeric characters: +//! $$ \Sigma_{cmd} = \\{x | 0 \leqslant x \leqslant 9\\} \cup \\{x | "a" \leqslant x \leqslant "z"\\} \cup \\{x | "A" \leqslant x \leqslant "Z"\\} \cup \\{"\\_", "\text{-}", "."\\} $$ +//! +//! ## Argument +//! Arguments are similar to the command, although they can also contain spaces and quotes, +//! if it's quoted and additional characters (here notated as "$\\dots{}$"): +//! $$ \Sigma_{args-quoted} = \Sigma_{cmd} \cup \\{"\\text{"}", "\\ ", \\dots{}\\} $$ +//! $$ \Sigma_{args-single-quoted} = \Sigma_{cmd} \cup \\{"'", "\\ ", \\dots{}\\} $$ +//! $$ \Sigma_{args} = \Sigma_{cmd} \cup \\{\\dots{}\\} $$ +//! Look at the [trinitry.pest](../../../src/trinitry.pest) file for a full list of the additional +//! allowed characters. +//! +//! # Examples +//! ## Command +//! A valid command would be something like that: +//! ```text +//! quit +//! ``` +//! something like that would not be valid however, as Trinitry does not support these 'complex' +//! language features: +//! ```text +//! write && quit +//! ``` +//! ## Arguments +//! A valid argumented command would be: +//! ```text +//! lua "function() print('Hi!') end" +//! ``` +//! Whilst this would not be valid (that is, it would very likely not be what you want): +//! ```text +//! lua "function() print("Hi!") end" +//! ``` +//! as the double quotes in the print statement actually unquote the argument, leaving you with +//! three arguments: +//! 1. `function() print(` +//! 1. `Hi!` +//! 1. `) end` +use std::fmt::Display; + +use pest::{error::Error, Parser}; +use pest_derive::Parser; + +#[derive(Parser)] +#[grammar = "trinitry.pest"] +pub struct Trinitry { + command: String, + arguments: Vec, +} + +impl Trinitry { + pub fn new(input: &str) -> Result> { + let parsed = Self::parse(Rule::trinitry, input)?; + + let command = { + let command: Vec<_> = parsed.clone().find_tagged("command").collect(); + + // Ensure that we have only one command + // This should be ensured by the grammar, thus the 'debug_assert' + debug_assert_eq!(command.len(), 1); + + // PERFORMANCE(@soispha): Replace this with `mem::take` (when pairs implements Default) + // <2023-11-01> + command + .first() + .expect("This should contain exactly one element") + .to_owned() + }; + let arguments: Vec<_> = parsed.clone().find_tagged("argument").collect(); + + Ok(Trinitry { + command: command.as_str().to_owned(), + arguments: arguments + .iter() + .map(|arg| { + let mut arg = arg.as_str().trim(); + arg = if let Some(new_arg) = arg.strip_prefix("\"") { + new_arg + } else { + arg + }; + arg = if let Some(new_arg) = arg.strip_suffix("\"") { + new_arg + } else { + arg + }; + + arg = if let Some(new_arg) = arg.strip_prefix("'") { + new_arg + } else { + arg + }; + arg = if let Some(new_arg) = arg.strip_suffix("'") { + new_arg + } else { + arg + }; + arg.to_owned() + }) + .collect(), + }) + } +} + +impl Display for Trinitry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.arguments.is_empty() { + f.write_str(&self.command) + } else { + f.write_fmt(format_args!( + "{} {}", + &self.command, + &self.arguments.join(" ") + )) + } + } +} + +mod tests; + +#[cfg(test)] +mod test { + use crate::Trinitry; + + #[test] + fn parse_cmd() { + let string = "quit"; + let p = Trinitry::new(string).unwrap_or_else(|e| { + panic!("{}", e); + }); + assert_eq!(&p.command, "quit"); + assert!(&p.arguments.is_empty()); + } + + #[test] + fn parse_arg_clean() { + let string = r##"lua print("Hi")"##; + let p = Trinitry::new(string).unwrap_or_else(|e| { + panic!("{}", e); + }); + assert_eq!(&p.command, "lua"); + assert_eq!(&p.arguments[0], r#"print("Hi")"#); + } + + #[test] + fn parse_arg_quote() { + let string = r##"write "some 'file' name""##; + let p = Trinitry::new(string).unwrap_or_else(|e| { + panic!("{}", e); + }); + assert_eq!(&p.command, "write"); + assert_eq!(&p.arguments[0], "some 'file' name"); + } + + #[test] + fn parse_arg_single_quote() { + let string = r##"write 'some "file" name'"##; + let p = Trinitry::new(string).unwrap_or_else(|e| { + panic!("{}", e); + }); + assert_eq!(&p.command, "write"); + assert_eq!(&p.arguments[0], "some \"file\" name"); + } + + #[test] + fn parse_arg_multi() { + let string = r##"write 'some "file" name' "other name" last"##; + let p = Trinitry::new(string).unwrap_or_else(|e| { + panic!("{}", e); + }); + + let expected_args = vec!["some \"file\" name", "other name", "last"] + .iter() + .map(|str| (*str).to_owned()) + .collect::>(); + + assert_eq!(&p.command, "write"); + assert_eq!(&p.arguments, &expected_args); + } +} diff --git a/trinitry/src/trinitry.pest b/trinitry/src/trinitry.pest new file mode 100644 index 0000000..c868a6d --- /dev/null +++ b/trinitry/src/trinitry.pest @@ -0,0 +1,25 @@ +chars = { ASCII_ALPHANUMERIC | "_" | "-" | "." } + +// TODO(@soispha): Are these all the valid characters? <2023-11-01> +argument_chars = { chars | "(" | ")" | "{" | "}" | "<" | ">" | "?" | "!" | "+" | "^" | "@" +| "&" | "*" | "~" | "|" | "=" | "," | "\\" | "/" } +whitespace = _{ " " } // lower case to avoid special treatment of 'WHITESPACE' + +quote = _{ "\"" } +q = _{ quote } + +single_quote = _{ "'" } +sq = _{ single_quote } + + + +command = { chars+ } + +arg_quoted = { q ~ (!q ~ (argument_chars | " " | "'" ))+ ~ q } +arg_single_quoted = { sq ~ (!sq ~ (argument_chars | " " | "\"" ))+ ~ sq } +arg = { (argument_chars | "\"" | "'")+ } + +argument = { whitespace+ ~ (arg_quoted | arg_single_quoted | arg )} + + +trinitry = { SOI ~ #command = command ~ (#argument = argument)* ~ EOI }