Compare commits
4 Commits
f46084d3fe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
7933d76a92
|
|||
|
ba08f8b9f7
|
|||
|
900cd48509
|
|||
|
bfcd414a14
|
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[env]
|
||||
RUST_LOG="magma=debug"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
**/target
|
||||
.idea
|
||||
Cargo.lock
|
||||
210
Cargo.lock
generated
210
Cargo.lock
generated
@@ -17,6 +17,15 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -67,6 +76,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
@@ -97,12 +112,24 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -140,7 +167,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -201,6 +228,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -242,6 +278,29 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -268,6 +327,16 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -335,6 +404,30 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -366,6 +459,12 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "magma"
|
||||
version = "0.1.0"
|
||||
@@ -374,6 +473,9 @@ dependencies = [
|
||||
"clap_mangen",
|
||||
"color-eyre",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"log",
|
||||
"magma-command",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"semver",
|
||||
@@ -382,6 +484,17 @@ dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "magma-command"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"log",
|
||||
"quartz_nbt",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
@@ -395,6 +508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -496,7 +610,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -515,6 +629,21 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -524,6 +653,31 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quartz_nbt"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf389329ba2dad9c6d898b7955a64e58c89dd52d04f4e2753b9d86eb5f49821"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"cesu8",
|
||||
"flate2",
|
||||
"quartz_nbt_macros",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quartz_nbt_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "289baa0c8a4d1f840d2de528a7f8c29e0e9af48b3018172b3edad4f716e8daed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
@@ -553,6 +707,35 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
version = "0.2.2"
|
||||
@@ -608,7 +791,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -663,6 +846,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -685,6 +874,17 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
@@ -713,7 +913,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -750,7 +950,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -3,14 +3,27 @@ name = "magma"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[workspace]
|
||||
members = [ "magma-command" ]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.54", features = ["cargo", "derive"] }
|
||||
clap_mangen = "0.2.31"
|
||||
pest = { version = "2.8.5", features = ["pretty-print"] }
|
||||
pest_derive = { version = "2.8.5", features = ["grammar-extras"] }
|
||||
color-eyre = "0.6.5"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
toml = "0.9.11"
|
||||
semver = { version = "1.0.27", features = ["serde"] }
|
||||
dirs = "6.0.0"
|
||||
env_logger = { version = "0.11.8", features = ["kv"] }
|
||||
|
||||
magma-command = { path = "./magma-command" }
|
||||
|
||||
color-eyre.workspace = true
|
||||
serde.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[workspace.dependencies]
|
||||
color-eyre = "0.6.5"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
log = { version = "0.4.29", features = ["std"] }
|
||||
|
||||
18734
assets/commands.json
Normal file
18734
assets/commands.json
Normal file
File diff suppressed because it is too large
Load Diff
7
magma-command/Cargo.lock
generated
Normal file
7
magma-command/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "magma-command"
|
||||
version = "0.1.0"
|
||||
12
magma-command/Cargo.toml
Normal file
12
magma-command/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "magma-command"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.149"
|
||||
quartz_nbt = { version = "0.2.9", features = ["serde"] }
|
||||
|
||||
serde.workspace = true
|
||||
color-eyre.workspace = true
|
||||
log.workspace = true
|
||||
51
magma-command/src/error.rs
Normal file
51
magma-command/src/error.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CommandError {
|
||||
EmptyCommand,
|
||||
IncompleteCommand {
|
||||
path: String,
|
||||
suggestions: Vec<String>,
|
||||
},
|
||||
TooManyArguments {
|
||||
path: String,
|
||||
},
|
||||
InvalidArgument {
|
||||
path: String,
|
||||
argument: String,
|
||||
expected: Vec<String>,
|
||||
},
|
||||
InvalidRedirect {
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for CommandError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::EmptyCommand => write!(f, "Empty command"),
|
||||
Self::IncompleteCommand { path, suggestions } => {
|
||||
write!(f, "Incomplete command: {}\nExpected one of: {}", path, suggestions.join(", "))
|
||||
}
|
||||
Self::TooManyArguments { path } => {
|
||||
write!(f, "Too many arguments for command: {}", path)
|
||||
}
|
||||
Self::InvalidArgument { path, argument, expected } => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid argument '{}' for command: {}\nExpected one of: {}",
|
||||
argument,
|
||||
path,
|
||||
expected.join(", ")
|
||||
)
|
||||
},
|
||||
Self::InvalidRedirect { from, to } => {
|
||||
write!(f, "Invalid redirect from '{}' to '{}'", from, to.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CommandError {}
|
||||
246
magma-command/src/lib.rs
Normal file
246
magma-command/src/lib.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
pub mod types;
|
||||
pub mod error;
|
||||
|
||||
use types::{CommandNode, CommandTree, ParserType, StringKind};
|
||||
use error::CommandError;
|
||||
|
||||
pub struct MinecraftCommandValidator {
|
||||
root: CommandNode,
|
||||
}
|
||||
|
||||
impl MinecraftCommandValidator {
|
||||
pub fn new() -> color_eyre::Result<Self> {
|
||||
let commands_json = include_str!("../../assets/commands.json");
|
||||
let tree: CommandTree = serde_json::from_str(commands_json)?;
|
||||
Ok(Self { root: tree.root })
|
||||
}
|
||||
|
||||
pub fn validate(&self, command: &str) -> Result<ValidationResult, CommandError> {
|
||||
let tokens = self.tokenize(command);
|
||||
if tokens.is_empty() {
|
||||
return Err(CommandError::EmptyCommand);
|
||||
}
|
||||
let result = self.validate_tokens(&tokens, &self.root, 0, Vec::new())?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn tokenize(&self, command: &str) -> Vec<String> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut in_quotes = false;
|
||||
let mut escape_next = false;
|
||||
|
||||
for ch in command.chars() {
|
||||
if escape_next {
|
||||
current.push(ch);
|
||||
escape_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
match ch {
|
||||
'\\' => escape_next = true,
|
||||
'"' => {
|
||||
in_quotes = !in_quotes;
|
||||
current.push(ch);
|
||||
}
|
||||
' ' | '\t' if !in_quotes => {
|
||||
if !current.is_empty() {
|
||||
tokens.push(current.clone());
|
||||
current.clear();
|
||||
}
|
||||
}
|
||||
_ => current.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if !current.is_empty() {
|
||||
tokens.push(current);
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
fn validate_tokens(
|
||||
&self,
|
||||
tokens: &[String],
|
||||
node: &CommandNode,
|
||||
index: usize,
|
||||
mut path: Vec<String>,
|
||||
) -> Result<ValidationResult, CommandError> {
|
||||
if index >= tokens.len() {
|
||||
return if node.is_executable() {
|
||||
Ok(ValidationResult {
|
||||
valid: true,
|
||||
path,
|
||||
suggestions: self.get_suggestions_from_node(node),
|
||||
})
|
||||
} else {
|
||||
Err(CommandError::IncompleteCommand {
|
||||
path: path.join(" "),
|
||||
suggestions: self.get_suggestions_from_node(node),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
let current_token = &tokens[index];
|
||||
let children = node.children();
|
||||
|
||||
if !node.redirects().is_empty() {
|
||||
let nodes = self.get_redirects(&node.redirects());
|
||||
for node in nodes {
|
||||
if let Ok(result) = self.validate_tokens(tokens, node, index, path.clone()) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
return Err(CommandError::InvalidRedirect {
|
||||
from: path.join(" "),
|
||||
to: node.redirects().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
if children.is_empty() {
|
||||
return if node.is_executable() {
|
||||
Err(CommandError::TooManyArguments {
|
||||
path: path.join(" "),
|
||||
})
|
||||
} else {
|
||||
Err(CommandError::IncompleteCommand {
|
||||
path: path.join(" "),
|
||||
suggestions: self.get_suggestions_from_node(node),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for child in children {
|
||||
if let CommandNode::Literal { name, .. } = child && name == current_token {
|
||||
path.push(current_token.clone());
|
||||
return self.validate_tokens(tokens, child, index + 1, path);
|
||||
}
|
||||
}
|
||||
self.parse_args(node, tokens, index, path)
|
||||
}
|
||||
|
||||
/// an empty string value will redirect to all children
|
||||
fn get_redirects(&self, name: &[String]) -> Vec<&CommandNode> {
|
||||
self.root.children().iter()
|
||||
.filter(
|
||||
|child|
|
||||
name.contains(child.name()) || name.contains(&String::from(""))
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_args(
|
||||
&self,
|
||||
node: &CommandNode,
|
||||
tokens: &[String],
|
||||
index: usize,
|
||||
mut path: Vec<String>
|
||||
) -> Result<ValidationResult, CommandError> {
|
||||
let token = &tokens[index];
|
||||
for child in node.children() {
|
||||
let CommandNode::Argument { name, parser, .. } = child else {
|
||||
continue;
|
||||
};
|
||||
let parser_type = ParserType::from_parser_info(parser);
|
||||
if self.is_greedy_parser(&parser_type)
|
||||
&& self.validate_argument(&tokens[index..].join(" "), &parser_type).is_ok()
|
||||
{
|
||||
path.push(format!("<{}>", name));
|
||||
return self.validate_tokens(tokens, child, tokens.len(), path);
|
||||
} else if self.validate_argument(token, &parser_type).is_ok() {
|
||||
path.push(format!("<{}>", name));
|
||||
return self.validate_tokens(tokens, child, index + 1, path);
|
||||
}
|
||||
}
|
||||
Err(CommandError::InvalidArgument {
|
||||
path: path.join(" "),
|
||||
argument: token.to_string(),
|
||||
expected: self.get_suggestions_from_node(node),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_greedy_parser(&self, parser_type: &ParserType) -> bool {
|
||||
matches!(
|
||||
parser_type,
|
||||
ParserType::String { kind: StringKind::GreedyPhrase }
|
||||
| ParserType::Message
|
||||
| ParserType::Component
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_argument(&self, value: &str, parser_type: &ParserType) -> Result<(), String> {
|
||||
match parser_type {
|
||||
ParserType::Bool => {
|
||||
if value == "true" || value == "false" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Expected 'true' or 'false'".to_string())
|
||||
}
|
||||
}
|
||||
ParserType::Integer { min, max } => {
|
||||
let num: i32 = value.parse().map_err(|_| "Invalid integer")?;
|
||||
if let Some(min) = min && (num as f64) < *min {
|
||||
return Err(format!("Value must be >= {}", min));
|
||||
}
|
||||
if let Some(max) = max && (num as f64) > *max {
|
||||
return Err(format!("Value must be <= {}", max));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ParserType::Float { min, max } | ParserType::Double { min, max } => {
|
||||
let num: f64 = value.parse().map_err(|_| "Invalid number")?;
|
||||
if let Some(min) = min && num < *min {
|
||||
return Err(format!("Value must be >= {}", min));
|
||||
}
|
||||
if let Some(max) = max && num > *max {
|
||||
return Err(format!("Value must be <= {}", max));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ParserType::Entity { .. } => {
|
||||
//TODO: have to check amount
|
||||
if value.starts_with('@') {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Invalid entity selector".to_string())
|
||||
}
|
||||
}
|
||||
ParserType::String { .. } => Ok(()),
|
||||
ParserType::Message => Ok(()),
|
||||
ParserType::Component => match quartz_nbt::snbt::parse(value) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Invalid component: {}", e)),
|
||||
},
|
||||
_ => {
|
||||
log::warn!("Unknown parser type: {:?}, for '{}'", parser_type, value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_suggestions_from_node(&self, node: &CommandNode) -> Vec<String> {
|
||||
let mut suggestions = Vec::new();
|
||||
|
||||
for child in node.children() {
|
||||
match child {
|
||||
CommandNode::Literal { name, .. } => suggestions.push(name.clone()),
|
||||
CommandNode::Argument { name, .. } => suggestions.push(format!("<{}>", name)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
suggestions
|
||||
}
|
||||
|
||||
pub fn get_all_commands(&self) -> Vec<String> {
|
||||
self.get_suggestions_from_node(&self.root)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValidationResult {
|
||||
pub valid: bool,
|
||||
pub path: Vec<String>,
|
||||
pub suggestions: Vec<String>,
|
||||
}
|
||||
75
magma-command/src/types/enums.rs
Normal file
75
magma-command/src/types/enums.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ParserType {
|
||||
Bool,
|
||||
Double { min: Option<f64>, max: Option<f64> },
|
||||
Float { min: Option<f64>, max: Option<f64> },
|
||||
Integer { min: Option<f64>, max: Option<f64> },
|
||||
Long { min: Option<f64>, max: Option<f64> },
|
||||
String { kind: StringKind },
|
||||
Entity { amount: EntityAmount, entity_type: EntityType },
|
||||
ScoreHolder { amount: ScoreHolderAmount },
|
||||
GameProfile,
|
||||
BlockPos,
|
||||
ColumnPos,
|
||||
Vec3,
|
||||
Vec2,
|
||||
BlockState,
|
||||
BlockPredicate,
|
||||
ItemStack,
|
||||
ItemPredicate,
|
||||
Color,
|
||||
Component,
|
||||
Message,
|
||||
Nbt,
|
||||
NbtTag,
|
||||
NbtPath,
|
||||
Objective,
|
||||
ObjectiveCriteria,
|
||||
Operation,
|
||||
Particle,
|
||||
Angle,
|
||||
Rotation,
|
||||
ScoreboardSlot,
|
||||
Swizzle,
|
||||
Team,
|
||||
ItemSlot,
|
||||
ResourceLocation { registry: Option<String> },
|
||||
Function,
|
||||
EntityAnchor,
|
||||
IntRange,
|
||||
FloatRange,
|
||||
Dimension,
|
||||
Gamemode,
|
||||
Time,
|
||||
ResourceOrTag { registry: Option<String> },
|
||||
Resource { registry: Option<String> },
|
||||
TemplateMirror,
|
||||
TemplateRotation,
|
||||
Uuid,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StringKind {
|
||||
SingleWord,
|
||||
QuotablePhrase,
|
||||
GreedyPhrase,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EntityAmount {
|
||||
Single,
|
||||
Multiple,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EntityType {
|
||||
Players,
|
||||
Entities,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScoreHolderAmount {
|
||||
Single,
|
||||
Multiple,
|
||||
}
|
||||
9
magma-command/src/types/mcmeta.rs
Normal file
9
magma-command/src/types/mcmeta.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub struct PackMcMeta {
|
||||
min_format: [u8; 2],
|
||||
max_format: [u8; 2],
|
||||
support_version: [u8; 2]
|
||||
}
|
||||
|
||||
pub struct FunctionTag {
|
||||
value: Vec<String>
|
||||
}
|
||||
88
magma-command/src/types/mod.rs
Normal file
88
magma-command/src/types/mod.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
mod enums;
|
||||
mod parser;
|
||||
mod mcmeta;
|
||||
|
||||
pub use enums::*;
|
||||
pub use parser::*;
|
||||
pub use mcmeta::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CommandTree {
|
||||
pub root: CommandNode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum CommandNode {
|
||||
Root {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
executable: bool,
|
||||
#[serde(default)]
|
||||
redirects: Vec<String>,
|
||||
#[serde(default)]
|
||||
children: Vec<CommandNode>,
|
||||
},
|
||||
Literal {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
executable: bool,
|
||||
#[serde(default)]
|
||||
redirects: Vec<String>,
|
||||
#[serde(default)]
|
||||
children: Vec<CommandNode>,
|
||||
},
|
||||
Argument {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
executable: bool,
|
||||
#[serde(default)]
|
||||
redirects: Vec<String>,
|
||||
#[serde(default)]
|
||||
children: Vec<CommandNode>,
|
||||
parser: ParserInfo,
|
||||
},
|
||||
}
|
||||
|
||||
impl CommandNode {
|
||||
pub fn name(&self) -> &String {
|
||||
match self {
|
||||
CommandNode::Root { name, .. } => name,
|
||||
CommandNode::Literal { name, .. } => name,
|
||||
CommandNode::Argument { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_executable(&self) -> bool {
|
||||
match self {
|
||||
CommandNode::Root { executable, .. } => *executable,
|
||||
CommandNode::Literal { executable, .. } => *executable,
|
||||
CommandNode::Argument { executable, .. } => *executable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn children(&self) -> &[CommandNode] {
|
||||
match self {
|
||||
CommandNode::Root { children, .. } => children,
|
||||
CommandNode::Literal { children, .. } => children,
|
||||
CommandNode::Argument { children, .. } => children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redirects(&self) -> &[String] {
|
||||
match self {
|
||||
CommandNode::Root { redirects, .. } => redirects,
|
||||
CommandNode::Literal { redirects, .. } => redirects,
|
||||
CommandNode::Argument { redirects, .. } => redirects,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parser(&self) -> Option<&ParserInfo> {
|
||||
match self {
|
||||
CommandNode::Argument { parser, .. } => Some(parser),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
67
magma-command/src/types/parser.rs
Normal file
67
magma-command/src/types/parser.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::types::{EntityAmount, EntityType, ParserType, ScoreHolderAmount, StringKind};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ParserInfo {
|
||||
pub parser: String,
|
||||
pub modifier: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ParserType {
|
||||
pub fn from_parser_info(info: &ParserInfo) -> Self {
|
||||
match info.parser.as_str() {
|
||||
"brigadier:bool" => ParserType::Bool,
|
||||
"brigadier:double" => ParserType::Double { min: None, max: None },
|
||||
"brigadier:float" => ParserType::Float { min: None, max: None },
|
||||
"brigadier:integer" => ParserType::Integer { min: None, max: None },
|
||||
"brigadier:long" => ParserType::Long { min: None, max: None },
|
||||
"brigadier:string" => ParserType::String { kind: StringKind::SingleWord },
|
||||
"minecraft:entity" => ParserType::Entity {
|
||||
amount: EntityAmount::Multiple,
|
||||
entity_type: EntityType::Entities,
|
||||
},
|
||||
"minecraft:score_holder" => ParserType::ScoreHolder {
|
||||
amount: ScoreHolderAmount::Multiple,
|
||||
},
|
||||
"minecraft:game_profile" => ParserType::GameProfile,
|
||||
"minecraft:block_pos" => ParserType::BlockPos,
|
||||
"minecraft:column_pos" => ParserType::ColumnPos,
|
||||
"minecraft:vec3" => ParserType::Vec3,
|
||||
"minecraft:vec2" => ParserType::Vec2,
|
||||
"minecraft:block_state" => ParserType::BlockState,
|
||||
"minecraft:block_predicate" => ParserType::BlockPredicate,
|
||||
"minecraft:item_stack" => ParserType::ItemStack,
|
||||
"minecraft:item_predicate" => ParserType::ItemPredicate,
|
||||
"minecraft:color" => ParserType::Color,
|
||||
"minecraft:component" => ParserType::Component,
|
||||
"minecraft:message" => ParserType::Message,
|
||||
"minecraft:nbt_compound_tag" => ParserType::Nbt,
|
||||
"minecraft:nbt_tag" => ParserType::NbtTag,
|
||||
"minecraft:nbt_path" => ParserType::NbtPath,
|
||||
"minecraft:objective" => ParserType::Objective,
|
||||
"minecraft:objective_criteria" => ParserType::ObjectiveCriteria,
|
||||
"minecraft:operation" => ParserType::Operation,
|
||||
"minecraft:particle" => ParserType::Particle,
|
||||
"minecraft:angle" => ParserType::Angle,
|
||||
"minecraft:rotation" => ParserType::Rotation,
|
||||
"minecraft:scoreboard_slot" => ParserType::ScoreboardSlot,
|
||||
"minecraft:swizzle" => ParserType::Swizzle,
|
||||
"minecraft:team" => ParserType::Team,
|
||||
"minecraft:item_slot" => ParserType::ItemSlot,
|
||||
"minecraft:resource_location" => ParserType::ResourceLocation { registry: None },
|
||||
"minecraft:function" => ParserType::Function,
|
||||
"minecraft:entity_anchor" => ParserType::EntityAnchor,
|
||||
"minecraft:int_range" => ParserType::IntRange,
|
||||
"minecraft:float_range" => ParserType::FloatRange,
|
||||
"minecraft:dimension" => ParserType::Dimension,
|
||||
"minecraft:gamemode" => ParserType::Gamemode,
|
||||
"minecraft:time" => ParserType::Time,
|
||||
"minecraft:resource_or_tag" => ParserType::ResourceOrTag { registry: None },
|
||||
"minecraft:resource" => ParserType::Resource { registry: None },
|
||||
"minecraft:template_mirror" => ParserType::TemplateMirror,
|
||||
"minecraft:template_rotation" => ParserType::TemplateRotation,
|
||||
"minecraft:uuid" => ParserType::Uuid,
|
||||
_ => ParserType::Unknown(info.parser.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ impl Cli {
|
||||
if !out_dir.exists() {
|
||||
fs::create_dir_all(&out_dir).await?;
|
||||
}
|
||||
let mut compiler = ProjectCompiler::new(config, out_dir);
|
||||
let compiler = ProjectCompiler::new(config, out_dir)?;
|
||||
compiler.run(&path).await?;
|
||||
} else {
|
||||
return Err(color_eyre::eyre::eyre!("Path must be a directory for compilation"));
|
||||
|
||||
@@ -8,15 +8,17 @@ pub(crate) struct ProjectCompiler {
|
||||
}
|
||||
|
||||
impl ProjectCompiler {
|
||||
pub fn new(config: MagmaProjectConfig, out_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
compiler: MagmaCompiler::new(config),
|
||||
pub fn new(config: MagmaProjectConfig, out_dir: PathBuf) -> color_eyre::Result<Self> {
|
||||
let instance = Self {
|
||||
compiler: MagmaCompiler::new(config)?,
|
||||
out_dir
|
||||
}
|
||||
};
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, path: &PathBuf) -> color_eyre::Result<()> {
|
||||
self.compiler.compile(path).await?;
|
||||
pub async fn run(self, path: &PathBuf) -> color_eyre::Result<()> {
|
||||
let _ = self.compiler.compile(path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,191 @@
|
||||
use std::cmp::PartialEq;
|
||||
use std::path::PathBuf;
|
||||
use color_eyre::eyre::eyre;
|
||||
use color_eyre::Help;
|
||||
use pest::error::ErrorVariant;
|
||||
use pest::iterators::{Pair, Pairs};
|
||||
use pest::Parser;
|
||||
use magma_command::error::CommandError;
|
||||
use magma_command::MinecraftCommandValidator;
|
||||
use crate::helpers;
|
||||
use crate::parser::{MagmaParser, Rule};
|
||||
use crate::types::{MagmaProjectConfig, McFunctionFile};
|
||||
use crate::types::{Decorator, MagmaProjectConfig, McFunctionFile, EXCLUSIVE_DECORATORS};
|
||||
|
||||
pub(crate) struct MagmaCompiler {
|
||||
out_functions: Vec<McFunctionFile>,
|
||||
command_validator: MinecraftCommandValidator,
|
||||
config: MagmaProjectConfig
|
||||
}
|
||||
|
||||
impl MagmaCompiler {
|
||||
pub(crate) fn new(config: MagmaProjectConfig) -> Self {
|
||||
Self {
|
||||
pub(crate) fn new(config: MagmaProjectConfig) -> color_eyre::Result<Self> {
|
||||
let instance = Self {
|
||||
out_functions: Vec::new(),
|
||||
command_validator: MinecraftCommandValidator::new()?,
|
||||
config
|
||||
}
|
||||
};
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
pub async fn compile(&mut self, path: &PathBuf) -> color_eyre::Result<()> {
|
||||
pub async fn compile(mut self, path: &PathBuf) -> color_eyre::Result<Vec<McFunctionFile>> {
|
||||
let main_file_path = path.join(&self.config.entrypoint);
|
||||
let main_file_content = tokio::fs::read_to_string(&main_file_path).await?;
|
||||
let parse_result = MagmaParser::parse(Rule::program, &main_file_content)?;
|
||||
let mut errors = Vec::new();
|
||||
for pair in parse_result {
|
||||
self.parse(pair)?;
|
||||
log::debug!("Parsing pair:\n{}", helpers::format_pair(pair.clone(), 4, true));
|
||||
let Err(e) = self.parse(pair) else {
|
||||
continue;
|
||||
};
|
||||
errors.push(e);
|
||||
}
|
||||
Ok(())
|
||||
if errors.len() == 1 {
|
||||
return Err(eyre!(errors[0].to_string()));
|
||||
} else if !errors.is_empty() {
|
||||
let mut report = eyre!("Multiple parsing errors:");
|
||||
for e in errors {
|
||||
report = report.section(e);
|
||||
}
|
||||
return Err(report);
|
||||
}
|
||||
Ok(self.out_functions)
|
||||
}
|
||||
|
||||
fn parse(&mut self, pair: Pair<Rule>) -> color_eyre::Result<()> {
|
||||
fn parse(&mut self, pair: Pair<Rule>) -> Result<Vec<McFunctionFile>, pest::error::Error<Rule>> {
|
||||
let mut out_functions = Vec::new();
|
||||
match pair.as_rule() {
|
||||
Rule::functionExpression => {
|
||||
let mut pairs = pair.into_inner();
|
||||
let identifier = pairs.next().unwrap();
|
||||
let args = if let Some(rule) = pairs.peek()
|
||||
&& rule.as_rule() == Rule::functionArgs
|
||||
{
|
||||
Some(pairs.next().unwrap())
|
||||
} else { None };
|
||||
let statements = if let Some(rule) = pairs.peek()
|
||||
&& rule.as_rule() == Rule::block
|
||||
{ unbox_rule(pairs.next().unwrap()) }
|
||||
else { None };
|
||||
self.parse_function(None, identifier, args, statements)?;
|
||||
let pairs = pair.into_inner();
|
||||
let identifier = pairs.find_first_tagged("name").unwrap();
|
||||
let args = pairs.find_first_tagged("args");
|
||||
let statements = pairs.find_first_tagged("block").unwrap().into_inner();
|
||||
let decorators = if let Some(decorators) = pairs.find_first_tagged("decorators") {
|
||||
match self.parse_decorators(decorators) {
|
||||
Ok(decorators) => decorators,
|
||||
Err(error) => return Err(error)
|
||||
}
|
||||
} else { Vec::with_capacity(0) };
|
||||
|
||||
let function_file = self.parse_function(&decorators, None, identifier, args, statements)?;
|
||||
out_functions.push(function_file);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
Ok(out_functions)
|
||||
}
|
||||
|
||||
fn parse_decorators(&self, decorators: Pair<Rule>) -> Result<Vec<Decorator>, pest::error::Error<Rule>> {
|
||||
let rules = decorators.into_inner();
|
||||
let mut parsed_decorators = Vec::<(Decorator, pest::Span)>::with_capacity(rules.len());
|
||||
for rule in rules {
|
||||
let rule = rule.into_inner().find_first_tagged("value").unwrap();
|
||||
let span = rule.as_span();
|
||||
let value = rule.as_str();
|
||||
let decorator_result = Decorator::try_from(value);
|
||||
let decorator = match decorator_result {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
let error = pest::error::Error::<Rule>::new_from_span(ErrorVariant::CustomError {
|
||||
message: e
|
||||
}, rule.as_span());
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
if parsed_decorators.iter().any(|(d, _)| d == &decorator) {
|
||||
let error = pest::error::Error::<Rule>::new_from_span(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!(
|
||||
"Decorator `{}` already exists", &decorator
|
||||
),
|
||||
},
|
||||
span,
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
for (existing, _) in &parsed_decorators {
|
||||
if Self::is_exclusive_pair(existing, &decorator) {
|
||||
let error = pest::error::Error::<Rule>::new_from_span(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!(
|
||||
"Decorator `{}` conflicts with `{}`", decorator, existing
|
||||
),
|
||||
},
|
||||
span,
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
parsed_decorators.push((decorator, span))
|
||||
}
|
||||
|
||||
Ok(
|
||||
parsed_decorators.into_iter()
|
||||
.map(|(decorator, _)| decorator)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
|
||||
fn is_exclusive_pair(a: &Decorator, b: &Decorator) -> bool {
|
||||
EXCLUSIVE_DECORATORS.iter().any(|pair| {
|
||||
(pair[0].eq(a) && pair[1].eq(b)) || (pair[0].eq(b) && pair[1].eq(a))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_function(
|
||||
&self,
|
||||
decorators: &[Decorator],
|
||||
namespace: Option<String>,
|
||||
identifier: Pair<Rule>,
|
||||
args: Option<Pair<Rule>>,
|
||||
statements: Option<Pairs<Rule>>
|
||||
) -> color_eyre::Result<McFunctionFile> {
|
||||
let mut file = McFunctionFile::new(namespace, identifier.as_str().to_string());
|
||||
let Some(statements) = statements else {
|
||||
return Ok(file);
|
||||
};
|
||||
statements: Pairs<Rule>
|
||||
) -> Result<McFunctionFile, pest::error::Error<Rule>> {
|
||||
log::debug!("Compiling '{}'", identifier.as_str());
|
||||
let mut file = McFunctionFile::new(namespace, identifier.as_str().to_string(), decorators);
|
||||
for statement in statements {
|
||||
let Some(statement) = self.parse_statement(statement)? else {
|
||||
continue;
|
||||
};
|
||||
file.add_line(statement);
|
||||
let statements = self.parse_statement(statement)?;
|
||||
file.add_lines(statements);
|
||||
}
|
||||
println!("{}", file.lines().join("\n"));
|
||||
log::debug!("Compile Result:\n{}", file.lines().join("\n"));
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn parse_statement(&self, statement: Pair<Rule>) -> color_eyre::Result<Option<String>> {
|
||||
fn parse_statement(&self, statement: Pair<Rule>) -> Result<Vec<String>, pest::error::Error<Rule>> {
|
||||
let statement = statement.into_inner().next().unwrap();
|
||||
let mut lines = Vec::new();
|
||||
match statement.as_rule() {
|
||||
Rule::commandLine => {
|
||||
let command = statement.into_inner().next().unwrap();
|
||||
Ok(Some(self.parse_command(command)?))
|
||||
lines.push(self.parse_command(command)?);
|
||||
},
|
||||
Rule::commandBlock => {
|
||||
let commands = statement.into_inner();
|
||||
for command in commands {
|
||||
lines.push(self.parse_command(command)?);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
Ok(None)
|
||||
_ => {}
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn parse_command(&self, command: Pair<Rule>) -> Result<String, pest::error::Error<Rule>> {
|
||||
let mut primitives = Vec::<String>::new();
|
||||
let span = command.as_span();
|
||||
for primitive in command.into_inner() {
|
||||
match primitive.as_rule() {
|
||||
Rule::string => {
|
||||
let string = helpers::unbox_string(primitive);
|
||||
primitives.push(string);
|
||||
}
|
||||
_ => { primitives.push(primitive.as_str().to_string())}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_command(&self, command: Pair<Rule>) -> color_eyre::Result<String> {
|
||||
Ok(command.as_str().to_string())
|
||||
let unboxed_command = primitives.join(" ");
|
||||
if let Err(e) = self.command_validator.validate(&unboxed_command) {
|
||||
return Err(pest::error::Error::<Rule>::new_from_span(ErrorVariant::CustomError {
|
||||
message: e.to_string()
|
||||
}, span));
|
||||
}
|
||||
Ok(unboxed_command)
|
||||
}
|
||||
}
|
||||
|
||||
fn unbox_rule(rule: Pair<Rule>) -> Option<Pairs<Rule>> {
|
||||
let mut block = rule.into_inner();
|
||||
if let Some(inner) = block.next() {
|
||||
Some(inner.into_inner())
|
||||
} else { None }
|
||||
}
|
||||
|
||||
@@ -8,15 +8,23 @@ fileScopeNamespaceExpression = {
|
||||
namespaceExpression = {
|
||||
"namespace" ~ namespaceIdentifier ~ "{" ~ functionExpression* ~ "}"
|
||||
}
|
||||
includeExpression = {
|
||||
"include" ~ string ~ ";"
|
||||
}
|
||||
decorators = {
|
||||
decoratorExpression*
|
||||
}
|
||||
decoratorExpression = {
|
||||
"@" ~ #value = identifier ~ NEWLINE?
|
||||
}
|
||||
|
||||
// function
|
||||
functionExpression = {
|
||||
"function" ~ identifier ~ "(" ~ functionArgs? ~ ")" ~ block
|
||||
#decorators = decorators? ~ "function" ~ #name = identifier ~ "(" ~ #args = functionArgs? ~ ")" ~ #block = block
|
||||
}
|
||||
functionArgs = {
|
||||
identifier ~ ("," ~ identifier)*
|
||||
}
|
||||
includeExpression = {
|
||||
"include" ~ string ~ ";"
|
||||
}
|
||||
|
||||
// rules
|
||||
block = { "{" ~ statements* ~ "}" }
|
||||
@@ -25,7 +33,7 @@ namespaceIdentifier = @{ identifier ~ ("." ~ identifier)* }
|
||||
identifier = @{ (ASCII_ALPHA) ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||
number = @{ "-"? ~ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
|
||||
string = @{ "\"" ~ ( escapeCharacters | !"\"" ~ ANY )* ~ "\"" }
|
||||
statements = { commandStatement | returnStatement | ifStatement }
|
||||
statements = { commandStatement | returnStatement | ifStatement | ifCommandStatement | assignmentStatement }
|
||||
|
||||
// statements
|
||||
ifStatement = {
|
||||
@@ -38,17 +46,24 @@ elseStatement = {
|
||||
"else" ~ block
|
||||
}
|
||||
|
||||
ifCommandStatement = {
|
||||
"if" ~ "command" ~ command ~ block ~ elseIfCommandStatement* ~ elseStatement?
|
||||
}
|
||||
elseIfCommandStatement = {
|
||||
"else" ~ "if" ~ "command" ~ command ~ block
|
||||
}
|
||||
|
||||
returnStatement = { "return" ~ value ~ ";" }
|
||||
assignmentStatement = { identifier ~ "=" ~ binaryExpression ~ ";" }
|
||||
commandStatement = _{ commandLine | commandBlock }
|
||||
|
||||
// mc commands
|
||||
commandLine = { "command" ~ command }
|
||||
commandBlock = { "command" ~ "{" ~ command* ~ "}" }
|
||||
commandLine = { "command" ~ command ~ ";" }
|
||||
commandBlock = { "command" ~ "{" ~ (command ~ ";")* ~ "}" }
|
||||
|
||||
command = { mcArg+ ~ ";" }
|
||||
command = { mcArg+ }
|
||||
mcArg = _{ nbtBlock | string | mcPrimitive }
|
||||
nbtBlock = { "{" ~ (nbtBlock | string | !("}" | "{") ~ ANY)* ~ "}" }
|
||||
nbtBlock = @{ "{" ~ (nbtBlock | string | !("}" | "{") ~ ANY)* ~ "}" }
|
||||
mcPrimitive = @{ (!(";" | "{" | "}" | "\"" | WHITESPACE) ~ ANY)+ }
|
||||
|
||||
// Binary operations with precedence (using PEG climbing)
|
||||
@@ -85,7 +100,7 @@ OR = { "||" }
|
||||
AND = { "&&" }
|
||||
|
||||
// constants
|
||||
escapeCharacters = @{ "\\" ~ ("\"" | "\\" | "n" | "r" | "t") }
|
||||
escapeCharacters = @{ "\\" ~ ANY }
|
||||
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
|
||||
COMMENT = _{ multiLineComment | singleLineComment }
|
||||
multiLineComment = { "/*" ~ (!"*/" ~ ANY)* ~ "*/" }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use pest::iterators::{Pair, Pairs};
|
||||
use crate::parser::Rule;
|
||||
|
||||
pub const FILE_EXTENSION: &str = "mg";
|
||||
|
||||
@@ -14,4 +16,72 @@ pub(crate) fn expand_tilde(path: PathBuf) -> color_eyre::Result<PathBuf> {
|
||||
return Ok(home.join(relative));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub(crate) fn expand_next_rule(mut rule: Pairs<Rule>) -> Option<Pairs<Rule>> {
|
||||
if let Some(inner) = rule.next() {
|
||||
Some(inner.into_inner())
|
||||
} else { None }
|
||||
}
|
||||
|
||||
pub(crate) fn unbox_string(string: Pair<Rule>) -> String {
|
||||
let str = string.as_str();
|
||||
str[1..str.len()-1].to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn format_pair(pair: Pair<Rule>, indent_level: usize, is_newline: bool) -> String {
|
||||
let indent = if is_newline {
|
||||
" ".repeat(indent_level)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let children: Vec<_> = pair.clone().into_inner().collect();
|
||||
let len = children.len();
|
||||
let children: Vec<_> = children
|
||||
.into_iter()
|
||||
.map(|pair| {
|
||||
format_pair(
|
||||
pair,
|
||||
if len > 1 {
|
||||
indent_level + 1
|
||||
} else {
|
||||
indent_level
|
||||
},
|
||||
len > 1,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let dash = if is_newline { "- " } else { "" };
|
||||
let pair_tag = match pair.as_node_tag() {
|
||||
Some(tag) => format!("(#{}) ", tag),
|
||||
None => String::new(),
|
||||
};
|
||||
match len {
|
||||
0 => format!(
|
||||
"{}{}{}{:?}: {:?}",
|
||||
indent,
|
||||
dash,
|
||||
pair_tag,
|
||||
pair.as_rule(),
|
||||
pair.as_span().as_str()
|
||||
),
|
||||
1 => format!(
|
||||
"{}{}{}{:?} > {}",
|
||||
indent,
|
||||
dash,
|
||||
pair_tag,
|
||||
pair.as_rule(),
|
||||
children[0]
|
||||
),
|
||||
_ => format!(
|
||||
"{}{}{}{:?}\n{}",
|
||||
indent,
|
||||
dash,
|
||||
pair_tag,
|
||||
pair.as_rule(),
|
||||
children.join("\n")
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ mod compiler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> color_eyre::Result<()> {
|
||||
env_logger::init();
|
||||
color_eyre::install()?;
|
||||
cli::Cli::parse().run().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,8 +2,4 @@ use pest_derive::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "grammar.pest"]
|
||||
pub struct MagmaParser;
|
||||
|
||||
impl MagmaParser {
|
||||
|
||||
}
|
||||
pub struct MagmaParser;
|
||||
38
src/types/decorator.rs
Normal file
38
src/types/decorator.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
pub const EXCLUSIVE_DECORATORS: [[Decorator; 2]; 1] = [
|
||||
[Decorator::Load, Decorator::Tick]
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Decorator {
|
||||
Tick, Load
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Decorator {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s {
|
||||
"tick" => Ok(Decorator::Tick),
|
||||
"load" => Ok(Decorator::Load),
|
||||
_ => Err(String::from("Invalid decorator type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for &Decorator {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
Decorator::Tick => "tick".to_string(),
|
||||
Decorator::Load => "load".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Decorator {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let value: String = self.into();
|
||||
write!(f, "{}", value)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ pub struct MagmaProjectConfig {
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Version {
|
||||
pub pack: [u8; 2],
|
||||
pub redoxide: semver::Version,
|
||||
pub magma: semver::Version,
|
||||
}
|
||||
|
||||
impl Default for MagmaProjectConfig {
|
||||
@@ -29,7 +29,7 @@ impl Default for Version {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pack: [94, 1],
|
||||
redoxide: semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
|
||||
magma: semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
use crate::types::decorator::Decorator;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct McFunctionFile {
|
||||
content: Vec<String>,
|
||||
namespace: Option<String>,
|
||||
name: String
|
||||
name: String,
|
||||
attributes: Vec<Decorator>
|
||||
}
|
||||
|
||||
impl McFunctionFile {
|
||||
pub fn new(namespace: Option<String>, name: String) -> Self {
|
||||
pub fn new(namespace: Option<String>, name: String, attributes: &[Decorator]) -> Self {
|
||||
Self {
|
||||
content: Vec::new(),
|
||||
namespace,
|
||||
name
|
||||
name,
|
||||
attributes: attributes.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +22,10 @@ impl McFunctionFile {
|
||||
self.content.push(line);
|
||||
}
|
||||
|
||||
pub fn add_lines(&mut self, lines: Vec<String>) {
|
||||
self.content.extend(lines);
|
||||
}
|
||||
|
||||
pub fn lines(&self) -> &Vec<String> {
|
||||
&self.content
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod magma_project_config;
|
||||
mod mcfunction_file;
|
||||
mod decorator;
|
||||
|
||||
pub use magma_project_config::*;
|
||||
pub(crate) use mcfunction_file::*;
|
||||
pub(crate) use mcfunction_file::*;
|
||||
pub use decorator::*;
|
||||
|
||||
Reference in New Issue
Block a user