diff --git a/Cargo.lock b/Cargo.lock index 104039f..a52e124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,21 +25,21 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -49,9 +49,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.59" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "shlex", @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -120,6 +120,63 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "databake" +version = "0.3.0" +source = "git+https://github.com/suprohub/databake#37cb31e7d616bef163d39d8412be71bc33e2950f" +dependencies = [ + "databake-derive", + "proc-macro2", + "quote", + "uuid", +] + +[[package]] +name = "databake-derive" +version = "0.3.0" +source = "git+https://github.com/suprohub/databake#37cb31e7d616bef163d39d8412be71bc33e2950f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -142,12 +199,42 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -173,9 +260,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -213,14 +300,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -233,11 +326,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -249,21 +343,21 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miniz_oxide" @@ -290,6 +384,40 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "ownable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be13fa10c77ab9e9dc89fb30d6727f40d740ff2caf256dc28fc1a99efcf585d" +dependencies = [ + "ownable-core", + "ownable-macro", +] + +[[package]] +name = "ownable-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc3131271b6bf6f636edd8d1c0762221650c28c37cfe61c151e46ef9b020d38" + +[[package]] +name = "ownable-macro" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5dc3663cb539af1af233d217a85ff56330c7b212b523128fcd9a5b4bd542ea" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "prettyplease" version = "0.2.37" @@ -326,9 +454,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom", @@ -337,9 +465,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rustc-hash" @@ -400,9 +528,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -413,9 +541,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" @@ -464,6 +592,24 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "supports-hyperlinks" version = "3.2.0" @@ -481,13 +627,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "text_components" version = "0.1.7" dependencies = [ "chrono", "colored", + "databake", "heck", + "memchr", + "ownable", "proc-macro2", "quote", "rand", @@ -495,6 +655,7 @@ dependencies = [ "serde", "serde_json", "simdnbt", + "smallvec", "supports-hyperlinks", "uuid", ] @@ -533,9 +694,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom", "js-sys", @@ -545,11 +706,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -558,14 +719,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" dependencies = [ "cfg-if", "once_cell", @@ -576,9 +737,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -586,9 +747,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" dependencies = [ "bumpalo", "proc-macro2", @@ -599,9 +760,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" dependencies = [ "unicode-ident", ] @@ -717,6 +878,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 141e4b0..e21d663 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ categories = [ custom = [] nbt = ["dep:simdnbt"] serde = ["dep:serde"] +ownable = ["dep:ownable"] +databake = ["dep:databake"] build = [ "dep:heck", "dep:proc-macro2", @@ -36,17 +38,21 @@ rand = { version = "0.10", default-features = false, features = [ "std", "thread_rng", ] } -serde = { version = "1.0.228", features = ["derive"], optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } simdnbt = { version = "0.10", optional = true } # Build dependencies -heck = { version = "0.5.0", optional = true } +heck = { version = "0.5", optional = true } proc-macro2 = { version = "1.0", optional = true } quote = { version = "1.0", optional = true } rustc-hash = { version = "2.1", optional = true } -serde_json = { version = "1.0.149", optional = true } +serde_json = { version = "1.0", optional = true } uuid = { version = "1.23", features = ["v4", "serde"] } -supports-hyperlinks = "3.2.0" +supports-hyperlinks = "3.2" +memchr = "2.8" +smallvec = "1.15" +ownable = { version = "1.0", optional = true } +databake = { git = "https://github.com/suprohub/databake", optional = true, features = ["derive", "uuid"] } [dev-dependencies] chrono = "0.4" -serde_json = "1.0.149" +serde_json = "1.0" diff --git a/README.md b/README.md index 3e9c8b4..cb96e79 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ println!("{:p}", component); - [x] Terminal integration - [x] Serde integration - [x] SimdNbt integration -- [ ] MiniMessages integration +- [ ] MiniMessage integration - [ ] Extensibility integration ### Test diff --git a/examples/main.rs b/examples/main.rs index 1dcd2f7..8a1ac5f 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -12,7 +12,7 @@ use text_components::custom::{CustomContent, CustomData, CustomRegistry, Payload #[cfg(feature = "nbt")] use text_components::nbt::{NbtBuilder, ToSNBT}; use text_components::{ - Modifier, TextComponent, + Modifier, RawTextComponent, content::{NbtSource, ObjectPlayer, Resolvable}, fmt::set_display_resolutor, format::Color, @@ -23,7 +23,7 @@ use text_components::{ use uuid::Uuid; struct EmptyResolutor; -impl TextResolutor for EmptyResolutor { +impl<'a> TextResolutor<'a> for EmptyResolutor { fn translate(&self, key: &str) -> Option { match key { "content" => Some(String::from( @@ -38,10 +38,10 @@ impl TextResolutor for EmptyResolutor { _ => None, } } - fn resolve_content(&self, resolvable: &Resolvable) -> TextComponent { + fn resolve_content(&self, resolvable: &Resolvable) -> RawTextComponent<'a> { match resolvable { - Resolvable::Scoreboard { .. } => TextComponent::plain("5"), - Resolvable::Entity { .. } => TextComponent::plain("MrMelther") + Resolvable::Scoreboard { .. } => RawTextComponent::plain("5"), + Resolvable::Entity { .. } => RawTextComponent::plain("MrMelther") .insertion("MrMelther") .click_event(ClickEvent::suggest_command("/msg MrMelther ")) .hover_event(HoverEvent::show_entity( @@ -50,7 +50,7 @@ impl TextResolutor for EmptyResolutor { Some("MrMelther"), )), #[cfg(feature = "nbt")] - Resolvable::NBT { .. } => TextComponent::plain( + Resolvable::NBT { .. } => RawTextComponent::plain( Nbt::Some(BaseNbt::new( "", NbtCompound::from_values(vec![ @@ -65,12 +65,12 @@ impl TextResolutor for EmptyResolutor { ), #[cfg(not(feature = "nbt"))] Resolvable::NBT { .. } => { - TextComponent::plain("{base:3.0d,id:\"minecraft:entity_interaction_range\"}") + RawTextComponent::plain("{base:3.0d,id:\"minecraft:entity_interaction_range\"}") } } } #[cfg(feature = "custom")] - fn resolve_custom(&self, data: &CustomData) -> Option { + fn resolve_custom(&self, data: &CustomData) -> Option> { if data.id == "time" { return Some(TimeContent.resolve((), Payload::Empty)); } @@ -78,14 +78,14 @@ impl TextResolutor for EmptyResolutor { } } #[cfg(feature = "custom")] -impl CustomRegistry for EmptyResolutor { +impl<'a> CustomRegistry<'a> for EmptyResolutor { type Data = (); - fn register_content(&mut self, _id: &'static str, _content: T) { + fn register_content>(&mut self, _id: &'a str, _content: T) { todo!() } - fn get_content(&self, _id: String) -> Box> { + fn get_content(&self, _id: String) -> Box> { Box::new(TimeContent) } } @@ -96,35 +96,32 @@ const RESOLUBLE: Translation<4> = Translation("resoluble"); #[cfg(feature = "custom")] struct TimeContent; #[cfg(feature = "custom")] -impl CustomContent for TimeContent { +impl<'a> CustomContent<'a> for TimeContent { type Reg = EmptyResolutor; - fn as_data(&self) -> CustomData { + fn as_data(&self) -> CustomData<'a> { CustomData { id: std::borrow::Cow::Borrowed("time"), payload: Payload::Empty, } } - fn resolve(&self, _data: (), _payload: Payload) -> TextComponent { - TextComponent::plain(Utc::now().format("%H:%M").to_string()) + fn resolve(&self, _data: (), _payload: Payload) -> RawTextComponent<'a> { + RawTextComponent::plain(Utc::now().format("%H:%M").to_string()) } } fn main() { set_display_resolutor(&EmptyResolutor); - let mut resolubles = RESOLUBLE + let resolubles = RESOLUBLE .message([ ObjectPlayer::name("MrMelther").reset(), - TextComponent::scoreboard("MrMelther", "objective").reset(), - TextComponent::entity("@p", None).reset(), - TextComponent::nbt("attributes[2]", NbtSource::entity("@p"), false, None).reset(), + RawTextComponent::scoreboard("MrMelther", "objective").reset(), + RawTextComponent::entity("@p", None).reset(), + RawTextComponent::nbt("attributes[2]", NbtSource::entity("@p"), false, None).reset(), ]) .color_hex("#6f00ff"); - #[cfg(feature = "custom")] - (&mut resolubles).add_children(vec!["\n Custom: ".into(), TimeContent.reset()]); - let component = CONTENT .message([ "This text is Blue!".reset().color(Color::Blue), @@ -144,8 +141,16 @@ fn main() { .reset(), ]) .color(Color::Green) - .bold(true) - .add_child(resolubles); + .bold(true); + + let component = cfg_select! { + feature = "custom" => { + component.add_child(resolubles.add_children(vec!["\n Custom: ".into(), TimeContent.reset()])) + } + _ => { + component + } + }; println!("\nDebug:\n{:?}", component); #[cfg(feature = "serde")] diff --git a/examples/nbt.rs b/examples/nbt.rs index 5fe74e9..24d4077 100644 --- a/examples/nbt.rs +++ b/examples/nbt.rs @@ -1,6 +1,6 @@ use simdnbt::owned::{BaseNbt, Nbt, NbtCompound, NbtTag}; use text_components::{ - Modifier, TextComponent, + Modifier, RawTextComponent, format::Color, interactivity::{ClickEvent, HoverEvent}, nbt::{NbtBuilder, ToSNBT}, @@ -19,7 +19,7 @@ fn main() -> Result<(), String> { ("string".into(), NbtTag::String("This is a text".into())), ]), )); - let component = TextComponent::nbt_display(nbt); + let component = RawTextComponent::nbt_display(nbt); println!( "tellraw @p {}", component.build(&NoResolutor, NbtBuilder).to_snbt() @@ -38,8 +38,8 @@ fn main() -> Result<(), String> { ]) .build(&NoResolutor, NbtBuilder); println!("{:?}", nbt); - let component = - TextComponent::from_nbt(&nbt).ok_or(String::from("Cannot recompose the TextComponent!"))?; + let component = RawTextComponent::from_nbt(&nbt) + .ok_or(String::from("Cannot recompose the TextComponent!"))?; println!("{:?}", component); println!("{:p}", component); Ok(()) diff --git a/examples/serde.rs b/examples/serde.rs index b9f9e83..4c8427d 100644 --- a/examples/serde.rs +++ b/examples/serde.rs @@ -1,11 +1,11 @@ -use text_components::{Modifier, TextComponent, format::Color, translation::TranslatedMessage}; +use text_components::{Modifier, RawTextComponent, format::Color, translation::TranslatedMessage}; fn main() { - let component: TextComponent = TranslatedMessage::new("key", None) + let component: RawTextComponent = TranslatedMessage::new("key", None) .color(Color::Blue) .bold(true); println!("{}", serde_json::to_string_pretty(&component).unwrap()); - let component: TextComponent = serde_json::from_str( + let component: RawTextComponent = serde_json::from_str( "{ \"text\": \"This is a Serde test\", \"color\": \"blue\", diff --git a/examples/snbt.rs b/examples/snbt.rs index f330818..b021e6c 100644 --- a/examples/snbt.rs +++ b/examples/snbt.rs @@ -1,4 +1,4 @@ -use text_components::TextComponent; +use text_components::RawTextComponent; fn main() { use std::io::{Write, stdin, stdout}; @@ -14,7 +14,7 @@ fn main() { if let Some('\r') = s.chars().next_back() { s.pop(); } - let component = TextComponent::from_snbt(&s); + let component = RawTextComponent::from_snbt(&s); match component { Ok(component) => { println!("{:?}", component); diff --git a/src/content.rs b/src/content.rs index 3c569cc..d73204a 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,30 +1,33 @@ #[cfg(feature = "custom")] use crate::custom::CustomData; use crate::{ - TextComponent, format::Format, interactivity::Interactivity, translation::TranslatedMessage, + RawTextComponent, format::Format, interactivity::Interactivity, translation::TranslatedMessage, }; use std::borrow::Cow; #[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::content))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case", untagged))] -pub enum Content { +pub enum Content<'a> { Text { - text: Cow<'static, str>, + text: Cow<'a, str>, }, - Translate(TranslatedMessage), + Translate(TranslatedMessage<'a>), Keybind { - keybind: Cow<'static, str>, + keybind: Cow<'a, str>, }, /// #### Needs [resolution](TextComponent::resolve) #[cfg(feature = "custom")] - Custom(CustomData), - Object(Object), + Custom(CustomData<'a>), + Object(Object<'a>), /// #### Needs [resolution](TextComponent::resolve) - Resolvable(Resolvable), + Resolvable(Resolvable<'a>), } -impl From for Content { +impl<'a> From for Content<'a> { fn from(value: String) -> Self { Content::Text { text: Cow::Owned(value), @@ -33,18 +36,21 @@ impl From for Content { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::content))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub enum Object { +pub enum Object<'a> { Atlas { #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - atlas: Option>, - sprite: Cow<'static, str>, + atlas: Option>, + sprite: Cow<'a, str>, }, Player { - player: ObjectPlayer, + player: ObjectPlayer<'a>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Clone::clone", default) @@ -53,13 +59,16 @@ pub enum Object { }, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::content))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct ObjectPlayer { +pub struct ObjectPlayer<'a> { #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - pub name: Option>, + pub name: Option>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) @@ -69,16 +78,16 @@ pub struct ObjectPlayer { feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - pub texture: Option>, + pub texture: Option>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Vec::is_empty", default) )] - pub properties: Vec, + pub properties: Vec>, } -impl ObjectPlayer { +impl<'a> ObjectPlayer<'a> { /// Creates a [ObjectPlayer] from a player's name. - pub fn name>>(name: T) -> Self { + pub fn name(name: impl Into>) -> Self { ObjectPlayer { name: Some(name.into()), id: None, @@ -96,7 +105,7 @@ impl ObjectPlayer { } } /// Creates a [ObjectPlayer] from the path to a texture of a resource pack. - pub fn texture>>(path: T) -> Self { + pub fn texture(path: impl Into>) -> Self { ObjectPlayer { name: None, id: None, @@ -107,9 +116,9 @@ impl ObjectPlayer { /// Creates a [ObjectPlayer] from a player's skin properties. /// * `value` - A [texture data json](https://minecraft.wiki/w/Mojang_API#Query_player's_skin_and_cape) encoded in Base64 /// * `signature` - An optional Mojang's signature, also encoded in Base64 - pub fn property>, R: Into>>( - value: T, - signature: Option, + pub fn property( + value: impl Into>, + signature: Option>>, ) -> Self { ObjectPlayer { name: None, @@ -131,36 +140,42 @@ impl ObjectPlayer { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::content))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct PlayerProperties { - pub name: Cow<'static, str>, - pub value: Cow<'static, str>, - pub signature: Option>, +pub struct PlayerProperties<'a> { + pub name: Cow<'a, str>, + pub value: Cow<'a, str>, + pub signature: Option>, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::content))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub enum Resolvable { +pub enum Resolvable<'a> { /// The selector must only accept 1 target /// #### Needs [resolution](TextComponent::resolve) #[cfg_attr(feature = "serde", serde(rename = "score"))] Scoreboard { #[cfg_attr(feature = "serde", serde(rename = "name"))] - selector: Cow<'static, str>, - objective: Cow<'static, str>, + selector: Cow<'a, str>, + objective: Cow<'a, str>, }, /// #### Needs [resolution](TextComponent::resolve) #[cfg_attr(feature = "serde", serde(untagged))] Entity { - selector: Cow<'static, str>, + selector: Cow<'a, str>, #[cfg_attr(feature = "serde", serde(default = "Resolvable::entity_separator"))] - separator: Box, + separator: Box>, }, /// #### Needs [resolution](TextComponent::resolve) #[cfg_attr(feature = "serde", serde(untagged))] NBT { #[cfg_attr(feature = "serde", serde(rename = "nbt"))] - path: Cow<'static, str>, + path: Cow<'a, str>, // This meants to represent that this component should be // replaced with the one inside the nbt selected if possible #[cfg_attr( @@ -169,14 +184,14 @@ pub enum Resolvable { )] interpret: Option, #[cfg_attr(feature = "serde", serde(default = "Resolvable::nbt_separator"))] - separator: Box, + separator: Box>, #[cfg_attr(feature = "serde", serde(flatten, default = "NbtSource::Entity"))] - source: NbtSource, + source: NbtSource<'a>, }, } -impl Resolvable { - pub fn entity_separator() -> Box { - Box::new(TextComponent { +impl<'a> Resolvable<'a> { + pub fn entity_separator() -> Box> { + Box::new(RawTextComponent { content: Content::Text { text: Cow::Borrowed(", "), }, @@ -187,8 +202,8 @@ impl Resolvable { ..Default::default() }) } - pub fn nbt_separator() -> Box { - Box::new(TextComponent { + pub fn nbt_separator() -> Box> { + Box::new(RawTextComponent { content: Content::Text { text: Cow::Borrowed(", "), }, @@ -198,16 +213,19 @@ impl Resolvable { } #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::content))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -pub enum NbtSource { - Entity(Cow<'static, str>), - Block(Cow<'static, str>), - Storage(Cow<'static, str>), +pub enum NbtSource<'a> { + Entity(Cow<'a, str>), + Block(Cow<'a, str>), + Storage(Cow<'a, str>), } -impl NbtSource { +impl<'a> NbtSource<'a> { /// Creates a [NbtSource] from a entity selector. - pub fn entity>>(selector: T) -> Self { + pub fn entity(selector: impl Into>) -> Self { NbtSource::Entity(selector.into()) } /// Creates a [NbtSource] from a block coordinates. @@ -215,14 +233,14 @@ impl NbtSource { NbtSource::Block(Cow::Owned(format!("{x} {y} {z}"))) } /// Creates a [NbtSource] from a Nbt Storage identifier. - pub fn storage>>(identifier: T) -> Self { + pub fn storage(identifier: impl Into>) -> Self { NbtSource::Storage(identifier.into()) } } -impl From for TextComponent { - fn from(value: Content) -> Self { - TextComponent { +impl<'a> From> for RawTextComponent<'a> { + fn from(value: Content<'a>) -> Self { + RawTextComponent { content: value, children: Vec::new(), format: Format::new(), @@ -230,9 +248,9 @@ impl From for TextComponent { } } } -impl From for TextComponent { - fn from(value: Object) -> Self { - TextComponent { +impl<'a> From> for RawTextComponent<'a> { + fn from(value: Object<'a>) -> Self { + RawTextComponent { content: Content::Object(value), children: Vec::new(), format: Format::new(), @@ -240,9 +258,9 @@ impl From for TextComponent { } } } -impl From for TextComponent { - fn from(value: ObjectPlayer) -> Self { - TextComponent { +impl<'a> From> for RawTextComponent<'a> { + fn from(value: ObjectPlayer<'a>) -> Self { + RawTextComponent { content: Content::Object(Object::Player { player: value, hat: true, @@ -253,9 +271,9 @@ impl From for TextComponent { } } } -impl From for TextComponent { - fn from(value: Resolvable) -> Self { - TextComponent { +impl<'a> From> for RawTextComponent<'a> { + fn from(value: Resolvable<'a>) -> Self { + RawTextComponent { content: Content::Resolvable(value), children: Vec::new(), format: Format::new(), diff --git a/src/custom.rs b/src/custom.rs index 473cc26..5fd448c 100644 --- a/src/custom.rs +++ b/src/custom.rs @@ -1,10 +1,13 @@ -use crate::TextComponent; +use crate::RawTextComponent; use std::borrow::Cow; #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::custom))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct CustomData { - pub id: Cow<'static, str>, +pub struct CustomData<'a> { + pub id: Cow<'a, str>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Payload::is_empty", default) @@ -13,6 +16,9 @@ pub struct CustomData { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::custom))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum Payload { #[default] @@ -25,29 +31,32 @@ impl Payload { } } -pub trait CustomRegistry { +pub trait CustomRegistry<'a> { type Data; - fn register_content(&mut self, id: &'static str, content: T); - fn get_content(&self, id: String) -> Box>; + fn register_content>(&mut self, id: &'a str, content: T); + fn get_content(&self, id: String) -> Box>; } -pub trait CustomContent { - type Reg: CustomRegistry; - fn as_data(&self) -> CustomData; - fn resolve(&self, data: ::Data, payload: Payload) - -> TextComponent; +pub trait CustomContent<'a> { + type Reg: CustomRegistry<'a>; + fn as_data(&self) -> CustomData<'a>; + fn resolve( + &self, + data: >::Data, + payload: Payload, + ) -> RawTextComponent<'a>; } -impl From for TextComponent { - fn from(value: CustomData) -> Self { - TextComponent { +impl<'a> From> for RawTextComponent<'a> { + fn from(value: CustomData<'a>) -> Self { + RawTextComponent { content: crate::content::Content::Custom(value), ..Default::default() } } } -impl From for TextComponent { +impl<'a, T: CustomContent<'a> + 'a> From for RawTextComponent<'a> { fn from(value: T) -> Self { - TextComponent::custom(value) + RawTextComponent::custom(value) } } diff --git a/src/fmt.rs b/src/fmt.rs index 32ca573..a382a07 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,5 +1,5 @@ use crate::{ - TextComponent, + RawTextComponent, content::{Content, Object}, format::{Color, Format}, interactivity::{ClickEvent, Interactivity}, @@ -62,10 +62,10 @@ const OBFUSCATION_CHARS: [char; 822] = [ pub struct TextBuilder; impl TextBuilder { - fn stringify_content( + fn stringify_content<'a, R: TextResolutor<'a> + ?Sized, S: BuildTarget<'a>>( target: &S, resolutor: &R, - component: &TextComponent, + component: &RawTextComponent<'a>, ) -> S::Result where S::Result: From + ToString + Display, @@ -83,10 +83,10 @@ impl TextBuilder { let parts = resolutor.split_translation(translated); let mut built_parts = vec![]; for (part, pos) in parts { - let component_part = TextComponent { + let component_part = RawTextComponent { content: part.into(), format: component.format.clone(), - ..TextComponent::new() + ..RawTextComponent::new() }; built_parts.push( target @@ -98,7 +98,7 @@ impl TextBuilder { && pos <= args.len() && let Some(arg) = args.get(pos - 1) { - let arg_part = TextComponent { + let arg_part = RawTextComponent { content: arg.content.clone(), children: arg.children.clone(), format: arg.format.mix(&component.format), @@ -126,12 +126,12 @@ impl TextBuilder { } } } -impl BuildTarget for TextBuilder { +impl<'a> BuildTarget<'a> for TextBuilder { type Result = String; - fn build_component( + fn build_component + ?Sized>( &self, resolutor: &R, - component: &TextComponent, + component: &RawTextComponent<'a>, ) -> String { Self::stringify_content(self, resolutor, component) + &component @@ -144,12 +144,12 @@ impl BuildTarget for TextBuilder { } pub struct PrettyTextBuilder; -impl BuildTarget for PrettyTextBuilder { +impl<'a> BuildTarget<'a> for PrettyTextBuilder { type Result = ColoredString; - fn build_component( + fn build_component + ?Sized>( &self, resolutor: &R, - component: &TextComponent, + component: &RawTextComponent<'a>, ) -> ColoredString { let mut final_text = TextBuilder::stringify_content(self, resolutor, component); @@ -161,7 +161,7 @@ impl BuildTarget for PrettyTextBuilder { .children .iter() .map(|child| { - let child = TextComponent { + let child = RawTextComponent { content: child.content.clone(), children: child.children.clone(), format: child.format.mix(&component.format), @@ -222,7 +222,7 @@ impl BuildTarget for PrettyTextBuilder { .children .iter() .map(|child| { - let child = TextComponent { + let child = RawTextComponent { content: child.content.clone(), children: child.children.clone(), format: child.format.mix(&component.format), @@ -237,41 +237,41 @@ impl BuildTarget for PrettyTextBuilder { } } -impl TextComponent { - pub fn to_plain(&self, resolutor: &R) -> String { +impl<'a> RawTextComponent<'a> { + pub fn to_plain + ?Sized>(&self, resolutor: &R) -> String { self.build(resolutor, TextBuilder) } - pub fn to_pretty(&self, resolutor: &R) -> ColoredString { + pub fn to_pretty + ?Sized>(&self, resolutor: &R) -> ColoredString { self.build(resolutor, PrettyTextBuilder) } } -static mut DISPLAY_RESOLUTOR: &dyn TextResolutor = &NoResolutor; +static mut DISPLAY_RESOLUTOR: &'static dyn for<'a> TextResolutor<'a> = + &NoResolutor as &'static dyn for<'a> TextResolutor<'a>; static mut INITIALIZED: bool = false; -pub fn set_display_resolutor(resolutor: &'static T) { +pub fn set_display_resolutor TextResolutor<'a>>(resolutor: &'static T) { unsafe { if !INITIALIZED { - DISPLAY_RESOLUTOR = resolutor; + DISPLAY_RESOLUTOR = resolutor as &'static dyn for<'a> TextResolutor<'a>; INITIALIZED = true; } } } -impl Display for TextComponent { +impl Display for RawTextComponent<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", unsafe { self.to_plain(DISPLAY_RESOLUTOR) }) } } -/// Clearly a Pointer, not 'p' because of pretty, OF COURSE -impl Pointer for TextComponent { +impl<'a> Pointer for RawTextComponent<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", unsafe { self.to_pretty(DISPLAY_RESOLUTOR) }) } } -impl Debug for TextComponent { +impl<'a> Debug for RawTextComponent<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut debug = f.debug_struct("TextComponent"); debug.field("content", &self.content); @@ -288,7 +288,7 @@ impl Debug for TextComponent { } } -impl Debug for Content { +impl<'a> Debug for Content<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::Text { text } => Debug::fmt(&text, f), @@ -302,7 +302,7 @@ impl Debug for Content { } } -impl Debug for Format { +impl<'a> Debug for Format<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(Color::White) = self.color && let Some(Cow::Borrowed("minecraft:default")) = self.font @@ -372,7 +372,7 @@ impl Debug for Format { } } -impl Debug for Interactivity { +impl<'a> Debug for Interactivity<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut debug = f.debug_map(); if self.insertion.is_some() { diff --git a/src/format.rs b/src/format.rs index cc89888..c2d77cf 100644 --- a/src/format.rs +++ b/src/format.rs @@ -2,8 +2,11 @@ use colored::{ColoredString, Colorize}; use std::{borrow::Cow, fmt::Display}; #[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::format))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct Format { +pub struct Format<'a> { #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) @@ -13,7 +16,7 @@ pub struct Format { feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - pub font: Option>, + pub font: Option>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) @@ -46,12 +49,12 @@ pub struct Format { pub shadow_color: Option, } -impl Default for Format { +impl<'a> Default for Format<'a> { fn default() -> Self { Self::new() } } -impl Format { +impl<'a> Format<'a> { pub const fn new() -> Self { Self { color: None, @@ -84,7 +87,7 @@ impl Format { } self } - pub fn font>>(mut self, font: F) -> Self { + pub fn font(mut self, font: impl Into>) -> Self { self.font = Some(font.into()); self } @@ -126,12 +129,12 @@ impl Format { self.shadow_color = None; self } - pub fn mix(&self, other: &Format) -> Format { + pub fn mix(&self, other: &Format<'a>) -> Format<'a> { Format { color: if self.color.is_some() { - self.color.clone() + self.color } else { - other.color.clone() + other.color }, font: if self.font.is_some() { self.font.clone() @@ -172,7 +175,10 @@ impl Format { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::format))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum Color { @@ -211,28 +217,31 @@ impl Color { } None } - pub fn colorize_text>(&self, text: T) -> ColoredString { + + pub fn colorize_text(&self, text: impl Into) -> ColoredString { + let text = text.into(); match self { - Color::Black => text.into().black(), - Color::DarkBlue => text.into().blue(), - Color::DarkGreen => text.into().green(), - Color::DarkAqua => text.into().cyan(), - Color::DarkRed => text.into().red(), - Color::DarkPurple => text.into().magenta(), - Color::Gold => text.into().yellow(), - Color::Gray => text.into().white(), - Color::DarkGray => text.into().bright_black(), - Color::Blue => text.into().bright_blue(), - Color::Green => text.into().bright_green(), - Color::Aqua => text.into().bright_cyan(), - Color::Red => text.into().bright_red(), - Color::LightPurple => text.into().bright_magenta(), - Color::Yellow => text.into().bright_yellow(), - Color::White => text.into().bright_white(), - Color::Rgb(r, g, b) => text.into().truecolor(*r, *g, *b), + Color::Black => text.black(), + Color::DarkBlue => text.blue(), + Color::DarkGreen => text.green(), + Color::DarkAqua => text.cyan(), + Color::DarkRed => text.red(), + Color::DarkPurple => text.magenta(), + Color::Gold => text.yellow(), + Color::Gray => text.white(), + Color::DarkGray => text.bright_black(), + Color::Blue => text.bright_blue(), + Color::Green => text.bright_green(), + Color::Aqua => text.bright_cyan(), + Color::Red => text.bright_red(), + Color::LightPurple => text.bright_magenta(), + Color::Yellow => text.bright_yellow(), + Color::White => text.bright_white(), + Color::Rgb(r, g, b) => text.truecolor(*r, *g, *b), } } } + impl Display for Color { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/src/interactivity.rs b/src/interactivity.rs index 7fd7c1b..e5b3866 100644 --- a/src/interactivity.rs +++ b/src/interactivity.rs @@ -1,18 +1,26 @@ use uuid::Uuid; -use crate::TextComponent; +use crate::RawTextComponent; #[cfg(feature = "custom")] use crate::custom::CustomData; use std::borrow::Cow; +/// Represents interactive elements of a text component, such as click and hover events, +/// and text insertion on shift-click. #[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::interactivity))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct Interactivity { +pub struct Interactivity<'a> { + /// The text to insert into the chat when the player shift-clicks this component. #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - pub insertion: Option>, + pub insertion: Option>, + + /// The action performed when the player clicks on this component. #[cfg_attr( feature = "serde", serde( @@ -21,7 +29,9 @@ pub struct Interactivity { default ) )] - pub click: Option, + pub click: Option>, + + /// The action performed when the player hovers over this component. #[cfg_attr( feature = "serde", serde( @@ -30,16 +40,16 @@ pub struct Interactivity { default ) )] - pub hover: Option, + pub hover: Option>, } -impl Default for Interactivity { +impl<'a> Default for Interactivity<'a> { fn default() -> Self { Self::new() } } -impl Interactivity { +impl<'a> Interactivity<'a> { pub const fn new() -> Self { Self { insertion: None, @@ -47,9 +57,11 @@ impl Interactivity { hover: None, } } + pub fn is_none(&self) -> bool { self.insertion.is_none() && self.click.is_none() && self.hover.is_none() } + pub fn mix(&self, other: &mut Self) { if self.insertion.is_some() { other.insertion = self.insertion.clone() @@ -63,112 +75,152 @@ impl Interactivity { } } +/// Defines an action that occurs when a text component is clicked. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::interactivity))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(tag = "action", rename_all = "snake_case"))] -pub enum ClickEvent { +pub enum ClickEvent<'a> { + /// Opens the given URL. OpenUrl { - url: Cow<'static, str>, + /// The URL to open. + url: Cow<'a, str>, }, + /// Runs a command as the player. RunCommand { - command: Cow<'static, str>, + /// The command to execute. + command: Cow<'a, str>, }, + /// Replaces the player's chat input with a command (without sending it). SuggestCommand { - command: Cow<'static, str>, + /// The command to suggest. + command: Cow<'a, str>, }, + /// Changes the current page in a book. ChangePage { + /// The page number to switch to (1-based, signed for protocol compatibility). page: i32, }, + /// Copies the given value to the clipboard. CopyToClipboard { - value: Cow<'static, str>, + /// The string to copy. + value: Cow<'a, str>, }, + /// Shows a custom dialog. ShowDialog { - dialog: Cow<'static, str>, + /// Either a dialog resource identifier or a full dialog definition. + dialog: Cow<'a, str>, }, + /// A custom click event, available only with the `custom` feature. #[cfg(feature = "custom")] - Custom(CustomData), + Custom(CustomData<'a>), } -impl ClickEvent { + +impl<'a> ClickEvent<'a> { /// Creates a [ClickEvent] that opens a url when triggered. - pub fn open_url>>(url: T) -> Self { + pub fn open_url(url: impl Into>) -> Self { ClickEvent::OpenUrl { url: url.into() } } + /// Creates a [ClickEvent] that runs a command when triggered. - pub fn run_command>>(command: T) -> Self { + pub fn run_command(command: impl Into>) -> Self { ClickEvent::RunCommand { command: command.into(), } } + /// Creates a [ClickEvent] that replaces the chat input with a command when triggered. - pub fn suggest_command>>(command: T) -> Self { + pub fn suggest_command(command: impl Into>) -> Self { ClickEvent::SuggestCommand { command: command.into(), } } + /// Creates a [ClickEvent] that changes the page of a book when triggered. pub fn change_page(page: u32) -> Self { ClickEvent::ChangePage { page: page as i32 } } + /// Creates a [ClickEvent] that copies it's content to the clipboard when triggered. - pub fn copy_to_clipboard>>(value: T) -> Self { + pub fn copy_to_clipboard(value: impl Into>) -> Self { ClickEvent::CopyToClipboard { value: value.into(), } } + /// Creates a [ClickEvent] that shows a custom dialog when triggered. /// * `dialog` - Either a dialog id or a dialog definition - pub fn show_dialog>>(dialog: T) -> Self { + pub fn show_dialog(dialog: impl Into>) -> Self { ClickEvent::ShowDialog { dialog: dialog.into(), } } } +/// Defines an action that occurs when hovering over a text component. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::interactivity))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(tag = "action", rename_all = "snake_case"))] -pub enum HoverEvent { +pub enum HoverEvent<'a> { + /// Displays a text component as a tooltip. ShowText { - value: Box, + /// The text component to display. + value: Box>, }, + /// Displays an item tooltip. ShowItem { - id: Cow<'static, str>, + /// The item identifier (e.g., `minecraft:stone`). + id: Cow<'a, str>, + /// Optional stack size to display. #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] count: Option, + /// Optional stringified item components (JSON). #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - components: Option>, + components: Option>, }, + /// Displays an entity tooltip. ShowEntity { + /// An optional custom name component to show instead of the entity's actual name. #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - name: Option>, - id: Cow<'static, str>, + name: Option>>, + /// The entity type identifier (e.g., `minecraft:creeper`). + id: Cow<'a, str>, + /// The UUID of the entity. + #[cfg_attr(feature = "ownable", ownable(clone))] uuid: Uuid, }, } -impl HoverEvent { + +impl<'a> HoverEvent<'a> { /// Creates a [HoverEvent] that will show a text component. - pub fn show_text>(text: T) -> Self { + pub fn show_text(text: impl Into>) -> Self { HoverEvent::ShowText { value: Box::new(text.into()), } } + /// Creates a [HoverEvent] that will show an item. /// * `id` - The id of the item /// * `count` - If [Some] shows the amount of items /// * `components` - An optional stringified version of the item's components - pub fn show_item>, R: Into>>( - id: T, + pub fn show_item( + id: impl Into>, count: Option, - components: Option, + components: Option>>, ) -> Self { HoverEvent::ShowItem { id: id.into(), @@ -176,14 +228,15 @@ impl HoverEvent { components: components.map(Into::into), } } + /// Creates a [HoverEvent] that will show an entity. /// * `id` - The id of the entity's type /// * `uuid` - The id of the targeted entity /// * `name` - If [Some] the name to display - pub fn show_entity>, R: Into>( - id: T, + pub fn show_entity( + id: impl Into>, uuid: Uuid, - name: Option, + name: Option>>, ) -> Self { HoverEvent::ShowEntity { name: name.map(|r| Box::new(r.into())), diff --git a/src/lib.rs b/src/lib.rs index 1ea0efc..e7978d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ use crate::{ }; use std::borrow::Cow; +extern crate self as text_components; + #[cfg(feature = "build")] pub mod build; pub mod content; @@ -84,26 +86,31 @@ pub mod translation; /// component.to_pretty(resolutor); /// ``` #[derive(Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct TextComponent { +pub struct RawTextComponent<'a> { #[cfg_attr(feature = "serde", serde(flatten))] - pub content: Content, + pub content: Content<'a>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Vec::is_empty", rename = "extra", default) )] - pub children: Vec, + pub children: Vec>, #[cfg_attr(feature = "serde", serde(flatten))] - pub format: Format, + pub format: Format<'a>, #[cfg_attr(feature = "serde", serde(flatten))] - pub interactions: Interactivity, + pub interactions: Interactivity<'a>, } +pub type TextComponent = RawTextComponent<'static>; + // Constructors -impl TextComponent { +impl<'a> RawTextComponent<'a> { /// Creates an empty [TextComponent], useful to make it the parent. pub const fn new() -> Self { - TextComponent { + RawTextComponent { content: Content::Text { text: Cow::Borrowed(""), }, @@ -119,8 +126,8 @@ impl TextComponent { /// // Results in "Test Component" /// TextComponent::const_plain("Test Component"); /// ``` - pub const fn const_plain(text: &'static str) -> Self { - TextComponent { + pub const fn const_plain(text: &'a str) -> Self { + RawTextComponent { content: Content::Text { text: Cow::Borrowed(text), }, @@ -140,8 +147,8 @@ impl TextComponent { /// ``` /// let component: TextComponent = "Test Component".into(); /// ``` - pub fn plain>>(text: T) -> Self { - TextComponent { + pub fn plain(text: impl Into>) -> Self { + RawTextComponent { content: Content::Text { text: text.into() }, children: vec![], format: Format::new(), @@ -170,8 +177,8 @@ impl TextComponent { /// // Results in "The Rust compiler was killed by you using magic". /// TextComponent::translated(DEATH_ATTACK_INDIRECT_MAGIC.message(["The Rust compiler", "you"])); /// ``` - pub const fn translated(message: TranslatedMessage) -> Self { - TextComponent { + pub const fn translated(message: TranslatedMessage<'a>) -> Self { + RawTextComponent { content: Content::Translate(message), children: vec![], format: Format::new(), @@ -187,11 +194,8 @@ impl TextComponent { /// // Displays the Diamond Sword sprite /// TextComponent::atlas("item/diamond_sword", Some("minecraft:items")); /// ``` - pub fn atlas>, R: Into>>( - sprite: T, - atlas: Option, - ) -> Self { - TextComponent { + pub fn atlas(sprite: impl Into>, atlas: Option>>) -> Self { + RawTextComponent { content: Content::Object(Object::Atlas { atlas: atlas.map(Into::into), sprite: sprite.into(), @@ -209,8 +213,8 @@ impl TextComponent { /// // Displays the head of Jeb_ /// TextComponent::player_head(ObjectPlayer::name("Jeb_"), true); /// ``` - pub const fn player_head(player: ObjectPlayer, hat: bool) -> Self { - TextComponent { + pub const fn player_head(player: ObjectPlayer<'a>, hat: bool) -> Self { + RawTextComponent { content: Content::Object(Object::Player { player, hat }), children: Vec::new(), format: Format::new(), @@ -228,11 +232,11 @@ impl TextComponent { /// TextComponent::scoreboard("@p", "deaths"); /// ``` /// #### Needs [resolution](TextComponent::resolve) - pub fn scoreboard>, R: Into>>( - selector: T, - objective: R, + pub fn scoreboard( + selector: impl Into>, + objective: impl Into>, ) -> Self { - TextComponent { + RawTextComponent { content: Content::Resolvable(Resolvable::Scoreboard { selector: selector.into(), objective: objective.into(), @@ -252,8 +256,8 @@ impl TextComponent { /// TextComponent::entity("@a", Some(" ".into())); /// ``` /// #### Needs [resolution](TextComponent::resolve) - pub fn entity>>(selector: T, separator: Option) -> Self { - TextComponent { + pub fn entity(selector: impl Into>, separator: Option) -> Self { + RawTextComponent { content: Content::Resolvable(Resolvable::Entity { selector: selector.into(), separator: match separator { @@ -278,13 +282,13 @@ impl TextComponent { /// TextComponent::nbt("Health", NbtSource::entity("@p"), false, None); /// ``` /// #### Needs [resolution](TextComponent::resolve) - pub fn nbt>>( - path: T, - source: NbtSource, + pub fn nbt( + path: impl Into>, + source: NbtSource<'a>, interpret: bool, separator: Option, ) -> Self { - TextComponent { + RawTextComponent { content: Content::Resolvable(Resolvable::NBT { path: path.into(), interpret: if interpret { Some(true) } else { None }, @@ -301,8 +305,8 @@ impl TextComponent { } #[cfg(feature = "custom")] - pub fn custom(content: T) -> TextComponent { - TextComponent { + pub fn custom(content: impl CustomContent<'a> + 'a) -> RawTextComponent<'a> { + RawTextComponent { content: Content::Custom(content.as_data()), children: vec![], format: Format::new(), @@ -311,43 +315,43 @@ impl TextComponent { } } -impl Default for TextComponent { +impl<'a> Default for RawTextComponent<'a> { fn default() -> Self { - TextComponent::new() + RawTextComponent::new() } } -impl From<&'static str> for TextComponent { - fn from(value: &'static str) -> Self { - TextComponent::const_plain(value) +impl<'a> From<&'a str> for RawTextComponent<'a> { + fn from(value: &'a str) -> Self { + RawTextComponent::const_plain(value) } } -impl From for TextComponent { +impl<'a> From for RawTextComponent<'a> { fn from(value: String) -> Self { - TextComponent::plain(value) + RawTextComponent::plain(value) } } -pub trait Modifier { +pub trait Modifier<'a> { type Output; /// Adds a child at the end of a text component - fn add_child>(self, child: T) -> Self::Output; + fn add_child>>(self, child: T) -> Self::Output; /// Appends a [vec] of [Into]<[TextComponent]> as children of this component - fn add_children>(self, children: Vec) -> Self::Output; + fn add_children>>(self, children: Vec) -> Self::Output; /// Sets the Shift+Click chat insertion string - fn insertion>>(self, insertion: T) -> Self::Output; + fn insertion>>(self, insertion: T) -> Self::Output; /// Sets the [ClickEvent] for this component - fn click_event(self, click: ClickEvent) -> Self::Output; + fn click_event(self, click: ClickEvent<'a>) -> Self::Output; /// Sets the [HoverEvent] for this component - fn hover_event(self, hover: HoverEvent) -> Self::Output; + fn hover_event(self, hover: HoverEvent<'a>) -> Self::Output; /// Sets the [Color] of this component /// * If you want to use a hex code check [color_hex](TextComponent::color_hex) fn color(self, color: Color) -> Self::Output; /// Sets the color of this component from a 6 digit hex color /// * If you want to use a predefined color check [color](TextComponent::color) - fn color_hex(self, color: &str) -> Self::Output; + fn color_hex(self, color: &'a str) -> Self::Output; /// Sets the font used to display this component - fn font>>(self, font: F) -> Self::Output; + fn font>>(self, font: F) -> Self::Output; /// Makes this component **bold** fn bold(self, value: bool) -> Self::Output; /// Makes this component *italic* @@ -364,14 +368,14 @@ pub trait Modifier { fn reset(self) -> Self::Output; } -impl + Sized> Modifier for T { - type Output = TextComponent; - fn add_child>(self, child: F) -> TextComponent { +impl<'a, T: Into> + Sized> Modifier<'a> for T { + type Output = RawTextComponent<'a>; + fn add_child>>(self, child: F) -> RawTextComponent<'a> { let mut component = self.into(); component.children.push(child.into()); component } - fn add_children>(self, children: Vec) -> TextComponent { + fn add_children>>(self, children: Vec) -> RawTextComponent<'a> { let mut component = self.into(); for child in children { component.children.push(child.into()); @@ -379,151 +383,154 @@ impl + Sized> Modifier for T { component } - fn insertion>>(self, insertion: R) -> TextComponent { + fn insertion>>(self, insertion: R) -> RawTextComponent<'a> { let mut component = self.into(); component.interactions.insertion = Some(insertion.into()); component } - fn click_event(self, click: ClickEvent) -> TextComponent { + fn click_event(self, click: ClickEvent<'a>) -> RawTextComponent<'a> { let mut component = self.into(); component.interactions.click = Some(click); component } - fn hover_event(self, hover: HoverEvent) -> TextComponent { + fn hover_event(self, hover: HoverEvent<'a>) -> RawTextComponent<'a> { let mut component = self.into(); component.interactions.hover = Some(hover); component } - fn color(self, color: Color) -> TextComponent { + fn color(self, color: Color) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.color(color); component } - fn color_hex(self, color: &str) -> TextComponent { + fn color_hex(self, color: &'a str) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.color_hex(color); component } - fn font>>(self, font: F) -> TextComponent { + fn font>>(self, font: F) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.font(font); component } - fn bold(self, value: bool) -> TextComponent { + fn bold(self, value: bool) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.bold(value); component } - fn italic(self, value: bool) -> TextComponent { + fn italic(self, value: bool) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.italic(value); component } - fn underlined(self, value: bool) -> TextComponent { + fn underlined(self, value: bool) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.underlined(value); component } - fn strikethrough(self, value: bool) -> TextComponent { + fn strikethrough(self, value: bool) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.strikethrough(value); component } - fn obfuscated(self, value: bool) -> TextComponent { + fn obfuscated(self, value: bool) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.obfuscated(value); component } - fn shadow_color(self, a: u8, r: u8, g: u8, b: u8) -> TextComponent { + fn shadow_color(self, a: u8, r: u8, g: u8, b: u8) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.shadow_color(a, r, g, b); component } - fn reset(self) -> TextComponent { + fn reset(self) -> RawTextComponent<'a> { let mut component = self.into(); component.format = component.format.reset(); component } } -impl<'a> Modifier for &'a mut TextComponent { - type Output = &'a mut TextComponent; - fn add_child>(self, child: T) -> &'a mut TextComponent { +impl<'a> Modifier<'a> for &'a mut RawTextComponent<'a> { + type Output = &'a mut RawTextComponent<'a>; + fn add_child>>(self, child: T) -> &'a mut RawTextComponent<'a> { self.children.push(child.into()); self } - fn add_children>(self, children: Vec) -> &'a mut TextComponent { + fn add_children>>( + self, + children: Vec, + ) -> &'a mut RawTextComponent<'a> { for child in children { self.children.push(child.into()); } self } - fn insertion>>(self, insertion: T) -> &'a mut TextComponent { + fn insertion>>(self, insertion: T) -> &'a mut RawTextComponent<'a> { self.interactions.insertion = Some(insertion.into()); self } - fn click_event(self, click: ClickEvent) -> &'a mut TextComponent { + fn click_event(self, click: ClickEvent<'a>) -> &'a mut RawTextComponent<'a> { self.interactions.click = Some(click); self } - fn hover_event(self, hover: HoverEvent) -> &'a mut TextComponent { + fn hover_event(self, hover: HoverEvent<'a>) -> &'a mut RawTextComponent<'a> { self.interactions.hover = Some(hover); self } - fn color(self, color: Color) -> &'a mut TextComponent { + fn color(self, color: Color) -> &'a mut RawTextComponent<'a> { self.format.color = Some(color); self } - fn color_hex(self, color: &str) -> &'a mut TextComponent { + fn color_hex(self, color: &str) -> &'a mut RawTextComponent<'a> { if let Some(color) = Color::from_hex(color) { self.format.color = Some(color); } self } - fn font>>(self, font: F) -> &'a mut TextComponent { + fn font>>(self, font: F) -> &'a mut RawTextComponent<'a> { self.format.font = Some(font.into()); self } - fn bold(self, value: bool) -> &'a mut TextComponent { + fn bold(self, value: bool) -> &'a mut RawTextComponent<'a> { self.format.bold = Some(value); self } - fn italic(self, value: bool) -> &'a mut TextComponent { + fn italic(self, value: bool) -> &'a mut RawTextComponent<'a> { self.format.italic = Some(value); self } - fn underlined(self, value: bool) -> &'a mut TextComponent { + fn underlined(self, value: bool) -> &'a mut RawTextComponent<'a> { self.format.underlined = Some(value); self } - fn strikethrough(self, value: bool) -> &'a mut TextComponent { + fn strikethrough(self, value: bool) -> &'a mut RawTextComponent<'a> { self.format.strikethrough = Some(value); self } - fn obfuscated(self, value: bool) -> &'a mut TextComponent { + fn obfuscated(self, value: bool) -> &'a mut RawTextComponent<'a> { self.format.obfuscated = Some(value); self } - fn shadow_color(self, a: u8, r: u8, g: u8, b: u8) -> &'a mut TextComponent { + fn shadow_color(self, a: u8, r: u8, g: u8, b: u8) -> &'a mut RawTextComponent<'a> { self.format.shadow_color = Some(Format::parse_shadow_color(a, r, g, b)); self } - fn reset(self) -> &'a mut TextComponent { + fn reset(self) -> &'a mut RawTextComponent<'a> { self.format.color = Some(Color::White); self.format.font = Some(Cow::Borrowed("minecraft:default")); self.format.bold = Some(false); @@ -535,3 +542,182 @@ impl<'a> Modifier for &'a mut TextComponent { self } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::resolving::{BuildTarget, NoResolutor, TextResolutor}; + use std::borrow::Cow; + + struct StringTarget; + impl<'a> BuildTarget<'a> for StringTarget { + type Result = String; + fn build_component + ?Sized>( + &self, + _resolutor: &R, + component: &RawTextComponent<'a>, + ) -> Self::Result { + fn extract_text(comp: &RawTextComponent) -> String { + let mut s = match &comp.content { + Content::Text { text } => text.to_string(), + Content::Translate(msg) => msg.key.to_string(), + _ => String::new(), + }; + for child in &comp.children { + s.push_str(&extract_text(child)); + } + s + } + extract_text(component) + } + } + + #[test] + fn test_new_empty_component() { + let component = TextComponent::new(); + assert_eq!( + component.content, + Content::Text { + text: Cow::Borrowed("") + } + ); + assert!(component.children.is_empty()); + } + + #[test] + fn test_plain_text() { + let component = TextComponent::plain("Hello"); + assert_eq!( + component.content, + Content::Text { + text: Cow::Borrowed("Hello") + } + ); + } + + #[test] + fn test_const_plain() { + let component = TextComponent::const_plain("World"); + assert_eq!( + component.content, + Content::Text { + text: Cow::Borrowed("World") + } + ); + } + + #[test] + fn test_player_head() { + let component = TextComponent::player_head(ObjectPlayer::name("Jeb_"), true); + assert!(matches!( + component.content, + Content::Object(Object::Player { .. }) + )); + } + + #[test] + fn test_scoreboard() { + let component = TextComponent::scoreboard("@p", "deaths"); + assert!(matches!( + component.content, + Content::Resolvable(Resolvable::Scoreboard { .. }) + )); + } + + #[test] + fn test_add_child_and_children() { + let parent = TextComponent::plain("Parent") + .add_child("Child1") + .add_children(vec!["Child2", "Child3"]); + assert_eq!(parent.children.len(), 3); + assert_eq!( + parent.children[0].content, + Content::Text { + text: Cow::Borrowed("Child1") + } + ); + } + + #[test] + fn test_insertion() { + let component = TextComponent::plain("text").insertion("insert me"); + assert_eq!( + component.interactions.insertion, + Some(Cow::Borrowed("insert me")) + ); + } + + #[test] + fn test_click_event() { + let event = ClickEvent::open_url("http://example.com"); + let component = TextComponent::plain("link").click_event(event.clone()); + assert_eq!(component.interactions.click, Some(event)); + } + + #[test] + fn test_hover_event() { + let hover = HoverEvent::show_text("tooltip"); + let component = TextComponent::plain("hover").hover_event(hover.clone()); + assert_eq!(component.interactions.hover, Some(hover)); + } + + #[test] + fn test_color_and_hex() { + let component = TextComponent::plain("colored").color_hex("#00ff00"); + assert!(component.format.color.is_some()); + } + + #[test] + fn test_formatting_bold_italic_underline() { + let component = TextComponent::plain("f") + .bold(true) + .italic(true) + .underlined(true) + .strikethrough(false); + assert_eq!(component.format.bold, Some(true)); + assert_eq!(component.format.italic, Some(true)); + assert_eq!(component.format.underlined, Some(true)); + assert_eq!(component.format.strikethrough, Some(false)); + } + + #[test] + fn test_reset_formatting() { + let component = TextComponent::plain("x") + .color(Color::Blue) + .bold(true) + .italic(true) + .reset(); + assert_eq!(component.format.color, Some(Color::White)); + assert_eq!(component.format.bold, Some(false)); + assert_eq!(component.format.italic, Some(false)); + assert_eq!( + component.format.font, + Some(Cow::Borrowed("minecraft:default")) + ); + } + + #[test] + fn test_resolve_scoreboard_noresolutor() { + let component = TextComponent::scoreboard("@p", "score"); + let resolved = component.resolve(&NoResolutor); + assert!( + matches!(resolved.content, Content::Text { text } if text.contains("[Score: score]")) + ); + } + + #[test] + fn test_resolve_entity_noresolutor() { + let component = TextComponent::entity("@a", None); + let resolved = component.resolve(&NoResolutor); + assert!( + matches!(resolved.content, Content::Text { text } if text.contains("[Entity: @a]")) + ); + } + + #[test] + fn test_build_plain_text() { + let component = TextComponent::plain("Hello world"); + let result = component.build(&NoResolutor, StringTarget); + assert_eq!(result, "Hello world"); + } +} diff --git a/src/nbt.rs b/src/nbt.rs index ab9ef67..fb1b1b1 100644 --- a/src/nbt.rs +++ b/src/nbt.rs @@ -1,7 +1,7 @@ #[cfg(feature = "custom")] use crate::custom::Payload; use crate::{ - Modifier, TextComponent, + Modifier, RawTextComponent, content::{Content, Object}, format::{Color, Format}, interactivity::{ClickEvent, HoverEvent, Interactivity}, @@ -15,12 +15,12 @@ use std::ops::Deref as _; pub struct NbtBuilder; -impl BuildTarget for NbtBuilder { +impl<'a> BuildTarget<'a> for NbtBuilder { type Result = NbtTag; - fn build_component( + fn build_component + ?Sized>( &self, resolutor: &R, - component: &TextComponent, + component: &RawTextComponent<'a>, ) -> NbtTag { let mut items = vec![]; component.content.to_compound(&mut items, self, resolutor); @@ -46,8 +46,8 @@ impl BuildTarget for NbtBuilder { } } -impl TextComponent { - pub fn nbt_display>(tag: T) -> Self { +impl<'a> RawTextComponent<'a> { + pub fn nbt_display(tag: impl Into) -> Self { let tag = tag.into(); match tag { NbtTag::Byte(n) => n @@ -84,7 +84,7 @@ impl TextComponent { children.push(", ".into()); } } - children.push(TextComponent::plain("]")); + children.push(RawTextComponent::plain("]")); component.add_children(children) } NbtTag::String(string) => { @@ -94,12 +94,12 @@ impl TextComponent { let component = "[".color(Color::White); let mut children = vec![]; for (i, tag) in nbt_list.as_nbt_tags().into_iter().enumerate() { - children.push(TextComponent::nbt_display(tag)); + children.push(RawTextComponent::nbt_display(tag)); if i + 1 != nbt_list.as_nbt_tags().len() { children.push(", ".into()); } } - children.push(TextComponent::plain("]")); + children.push(RawTextComponent::plain("]")); component.add_children(children) } NbtTag::Compound(compound) => { @@ -111,14 +111,14 @@ impl TextComponent { children.push(name.to_string().color(Color::Aqua)); children.push(": ".into()); } else if len == 1 { - return TextComponent::nbt_display(tag); + return RawTextComponent::nbt_display(tag); } - children.push(TextComponent::nbt_display(tag)); + children.push(RawTextComponent::nbt_display(tag)); if i + 1 != len { children.push(", ".into()); } } - children.push(TextComponent::plain("}")); + children.push(RawTextComponent::plain("}")); component.add_children(children) } NbtTag::IntArray(items) => { @@ -132,7 +132,7 @@ impl TextComponent { children.push(", ".into()); } } - children.push(TextComponent::plain("]")); + children.push(RawTextComponent::plain("]")); component.add_children(children) } NbtTag::LongArray(items) => { @@ -150,7 +150,7 @@ impl TextComponent { children.push(", ".into()); } } - children.push(TextComponent::plain("]")); + children.push(RawTextComponent::plain("]")); component.add_children(children) } } @@ -264,8 +264,8 @@ impl ToSNBT for NbtTag { } } -impl Content { - fn to_compound( +impl<'a> Content<'a> { + fn to_compound + ?Sized>( &self, compound: &mut Vec<(Mutf8String, NbtTag)>, target: &NbtBuilder, @@ -347,7 +347,7 @@ impl Content { } } -impl Format { +impl<'a> Format<'a> { fn to_compound(&self, compound: &mut Vec<(Mutf8String, NbtTag)>) { if let Some(color) = &self.color { compound.push(( @@ -399,8 +399,8 @@ impl Format { } } -impl Interactivity { - fn to_compound( +impl<'a> Interactivity<'a> { + fn to_compound + ?Sized>( &self, resolutor: &R, compound: &mut Vec<(Mutf8String, NbtTag)>, @@ -420,8 +420,8 @@ impl Interactivity { } } -impl HoverEvent { - fn to_nbt_tag(&self, resolutor: &R) -> NbtTag { +impl<'a> HoverEvent<'a> { + fn to_nbt_tag + ?Sized>(&self, resolutor: &R) -> NbtTag { match self { HoverEvent::ShowText { value } => NbtTag::Compound(NbtCompound::from_values(vec![ ("action".into(), NbtTag::String("show_text".into())), @@ -466,7 +466,7 @@ impl HoverEvent { } } -impl ClickEvent { +impl<'a> ClickEvent<'a> { fn to_nbt_tag(&self) -> NbtTag { let mut values = vec![]; match &self { @@ -507,19 +507,19 @@ impl ClickEvent { } } -impl ToNbtTag for TextComponent { +impl<'a> ToNbtTag for RawTextComponent<'a> { fn to_nbt_tag(self) -> NbtTag { NbtBuilder.build_component(&NoResolutor, &self) } } -impl ToNbtTag for &TextComponent { +impl<'a> ToNbtTag for &RawTextComponent<'a> { fn to_nbt_tag(self) -> NbtTag { NbtBuilder.build_component(&NoResolutor, self) } } -impl FromNbtTag for TextComponent { +impl<'a> FromNbtTag for RawTextComponent<'a> { fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option { - TextComponent::from_nbt(&tag.to_owned()) + RawTextComponent::from_nbt(&tag.to_owned()) } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 9864e08..e75dfdc 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "custom")] use crate::custom::{CustomData, Payload}; use crate::{ - Modifier, TextComponent, + Modifier, RawTextComponent, content::{Content, NbtSource, Object, ObjectPlayer, PlayerProperties, Resolvable}, format::{Color, Format}, interactivity::{ClickEvent, HoverEvent, Interactivity}, @@ -56,13 +56,16 @@ impl Display for SnbtError { pub type SnbtResult = Result; -impl TextComponent { - pub fn from_snbt(string: &str) -> SnbtResult { +impl<'a> RawTextComponent<'a> { + pub fn from_snbt(string: &'a str) -> SnbtResult> { parse_body(None, &mut string.chars().peekable()) } } -fn parse_body(first: Option, chars: &mut Peekable) -> SnbtResult { +fn parse_body<'a>( + first: Option, + chars: &mut Peekable, +) -> SnbtResult> { let char = match first { Some(first) => first, None => { @@ -82,8 +85,8 @@ fn parse_body(first: Option, chars: &mut Peekable) -> SnbtResult return parse_string('"', chars).map(TextComponent::plain), - '\'' => return parse_string('\'', chars).map(TextComponent::plain), + '"' => return parse_string('"', chars).map(RawTextComponent::plain), + '\'' => return parse_string('\'', chars).map(RawTextComponent::plain), '[' => { let mut components = parse_vec(chars)?.into_iter(); let first = components.next().ok_or(SnbtError::Required( @@ -123,7 +126,7 @@ fn parse_string(opener: char, chars: &mut Peekable) -> SnbtResult Err(SnbtError::EndedAbruptely(line!())) } -fn parse_vec(chars: &mut Peekable) -> SnbtResult> { +fn parse_vec<'a>(chars: &mut Peekable) -> SnbtResult>> { let mut component = vec![]; let Ok(child) = parse_body(None, chars) else { return Err(SnbtError::UnfinishedComponent(line!())); @@ -145,14 +148,14 @@ fn parse_vec(chars: &mut Peekable) -> SnbtResult> { Err(SnbtError::EndedAbruptely(line!())) } -struct CompoundParts { +struct CompoundParts<'a> { pub content: String, pub object: String, - pub contents: [Option; 9], + pub contents: [Option>; 9], pub nbt: String, - pub nbt_sources: [Option; 3], + pub nbt_sources: [Option>; 3], } -impl CompoundParts { +impl<'a> CompoundParts<'a> { pub fn new() -> Self { CompoundParts { content: String::new(), @@ -164,7 +167,7 @@ impl CompoundParts { } } -fn parse_compound(chars: &mut Peekable) -> SnbtResult { +fn parse_compound<'a>(chars: &mut Peekable) -> SnbtResult> { let mut compound = CompoundParts::new(); let mut format = Format::new(); let mut interactions = Interactivity::new(); @@ -177,7 +180,7 @@ fn parse_compound(chars: &mut Peekable) -> SnbtResult { } match char { '}' => { - return Ok(TextComponent { + return Ok(RawTextComponent { content: retrieve_content(compound)?, children, format, @@ -514,7 +517,7 @@ fn match_content( } } -fn parse_scoreboard(chars: &mut Peekable) -> SnbtResult { +fn parse_scoreboard<'a>(chars: &mut Peekable) -> SnbtResult> { let mut selector = None; let mut objective = None; let mut name = String::new(); @@ -578,7 +581,7 @@ fn parse_scoreboard(chars: &mut Peekable) -> SnbtResult { } Err(SnbtError::EndedAbruptely(line!())) } -fn parse_player(chars: &mut Peekable) -> SnbtResult { +fn parse_player<'a>(chars: &mut Peekable) -> SnbtResult> { let mut player = ObjectPlayer { name: None, id: None, @@ -675,7 +678,7 @@ fn parse_player(chars: &mut Peekable) -> SnbtResult { } Err(SnbtError::EndedAbruptely(line!())) } -fn parse_player_property(chars: &mut Peekable) -> SnbtResult { +fn parse_player_property<'a>(chars: &mut Peekable) -> SnbtResult> { let mut property = PlayerProperties { name: Cow::Borrowed("-None-"), value: Cow::Borrowed("-None-"), @@ -744,7 +747,7 @@ fn parse_player_property(chars: &mut Peekable) -> SnbtResult) -> SnbtResult { +fn parse_custom<'a>(chars: &mut Peekable) -> SnbtResult> { let mut id = None; let mut name = String::new(); let mut in_name = true; @@ -802,7 +805,7 @@ fn parse_custom(chars: &mut Peekable) -> SnbtResult { Err(SnbtError::EndedAbruptely(line!())) } -fn retrieve_content(compound: CompoundParts) -> SnbtResult { +fn retrieve_content<'a>(compound: CompoundParts<'a>) -> SnbtResult> { let mut error = SnbtError::MissingContent; let pos = match compound.content.as_str() { "text" => Some(0), @@ -842,11 +845,11 @@ fn retrieve_content(compound: CompoundParts) -> SnbtResult { } Err(error) } -fn match_content_type( - mut content: Content, +fn match_content_type<'a>( + mut content: Content<'a>, nbt: &str, - nbt_sources: &[Option; 3], -) -> SnbtResult { + nbt_sources: &[Option>; 3], +) -> SnbtResult> { match &mut content { Content::Translate(msg) => { if !msg.key.is_empty() { @@ -1083,7 +1086,7 @@ fn match_interactions( } } -fn parse_click(chars: &mut Peekable) -> SnbtResult { +fn parse_click<'a>(chars: &mut Peekable) -> SnbtResult> { let mut action = String::new(); let mut events = [None, None, None, None, None, None, None, None]; let mut name = String::new(); @@ -1188,7 +1191,7 @@ fn parse_click(chars: &mut Peekable) -> SnbtResult { }) } "command" => { - let command: Cow<'static, str> = + let command: Cow<'a, str> = Cow::Owned(parse_string(next, chars)?); events[2] = Some(ClickEvent::RunCommand { command: command.clone(), @@ -1237,7 +1240,7 @@ fn parse_click(chars: &mut Peekable) -> SnbtResult { } Err(SnbtError::EndedAbruptely(line!())) } -fn parse_hover(chars: &mut Peekable) -> SnbtResult { +fn parse_hover<'a>(chars: &mut Peekable) -> SnbtResult> { let mut action = String::new(); let mut events = [None, None, None]; let mut name = String::new(); @@ -1320,8 +1323,7 @@ fn parse_hover(chars: &mut Peekable) -> SnbtResult { } "id" => match next { '\'' | '"' => { - let new_id: Cow<'static, str> = - Cow::Owned(parse_string(next, chars)?); + let new_id: Cow<'a, str> = Cow::Owned(parse_string(next, chars)?); match &mut events[1] { Some(HoverEvent::ShowItem { id, .. }) => { *id = new_id.clone(); diff --git a/src/parse/nbt.rs b/src/parse/nbt.rs index 92e076f..da05727 100644 --- a/src/parse/nbt.rs +++ b/src/parse/nbt.rs @@ -53,7 +53,7 @@ impl TextComponent { } } -impl Content { +impl<'a> Content<'a> { fn from_compound(compound: &NbtCompound) -> Option { if let Some(tag) = compound.get("text") && let NbtTag::String(text) = tag @@ -297,7 +297,7 @@ impl Content { } } -impl Format { +impl<'a> Format<'a> { fn from_compound(compound: &NbtCompound) -> Self { let mut format = Format::new(); if let Some(tag) = compound.get("color") @@ -425,7 +425,7 @@ impl Format { } } -impl Interactivity { +impl<'a> Interactivity<'a> { fn from_compound(compound: &NbtCompound) -> Self { let mut interaction = Interactivity::new(); if let Some(tag) = compound.get("insertion") @@ -448,7 +448,7 @@ impl Interactivity { } } -impl HoverEvent { +impl<'a> HoverEvent<'a> { fn from_compound(compound: &NbtCompound) -> Option { let tag = compound.get("action")?; if let NbtTag::String(event) = tag { @@ -529,7 +529,7 @@ impl HoverEvent { } } -impl ClickEvent { +impl<'a> ClickEvent<'a> { fn from_compound(compound: &NbtCompound) -> Option { let tag = compound.get("action")?; if let NbtTag::String(event) = tag { @@ -602,7 +602,7 @@ impl ClickEvent { } #[cfg(feature = "custom")] -impl CustomData { +impl<'a> CustomData<'a> { fn from_compound(compound: &NbtCompound) -> Option { use crate::custom::{CustomData, Payload}; diff --git a/src/resolving.rs b/src/resolving.rs index 93fbb29..de26e1f 100644 --- a/src/resolving.rs +++ b/src/resolving.rs @@ -3,19 +3,61 @@ use std::sync::Arc; #[cfg(feature = "custom")] use crate::custom::CustomData; use crate::{ - TextComponent, + RawTextComponent, content::{Content, Resolvable}, }; -/// Recommendation: Implement this on the World and Player -pub trait TextResolutor { - fn resolve_other(&self, content: &Content) -> TextComponent { - TextComponent::from(content.clone()) +/// Trait for resolving dynamic content within a text component. +/// +/// This trait provides the necessary hooks to replace placeholders (like scoreboards, +/// entity selectors, NBT paths, translations, and custom data) with actual +/// `RawTextComponent` trees. It is intended to be implemented on game-world objects +/// such as a player or the server world, where the resolution logic can access +/// live data. +/// +/// # Recommendation +/// Implement this on the `World` and `Player` types in your application. +pub trait TextResolutor<'a> { + /// Fallback resolution for any `Content` variant that is not explicitly handled. + /// + /// By default it clones the content as-is, converting it into a plain component. + /// Override this method if you need special handling for other content types. + fn resolve_other(&self, content: &Content<'a>) -> RawTextComponent<'a> { + RawTextComponent::from(content.clone()) } - fn resolve_content(&self, resolvable: &Resolvable) -> TextComponent; + + /// Resolves a `Resolvable` variant into a `RawTextComponent`. + /// + /// This method is called for scoreboards, entity selectors, and NBT paths. + /// The implementation should query the game state and return a component + /// representing the resolved value (e.g., a score value, an entity name, + /// or a formatted NBT tag). + fn resolve_content(&self, resolvable: &Resolvable<'a>) -> RawTextComponent<'a>; + + /// Resolves a custom data block into an optional `RawTextComponent`. + /// + /// Only available when the `custom` feature is enabled. Return `None` if + /// the custom ID is not recognized or cannot be resolved. #[cfg(feature = "custom")] - fn resolve_custom(&self, data: &CustomData) -> Option; + fn resolve_custom(&self, data: &CustomData<'a>) -> Option>; + + /// Translates a given translation key into a human-readable string. + /// + /// Returns `None` if the key is unknown. The returned string may contain + /// parameter placeholders (`%s` or `%n$s`) that will be processed by + /// `split_translation`. fn translate(&self, key: &str) -> Option; + + /// Splits a translation string into segments and parameter indices. + /// + /// This method parses placeholders like `%s` (sequential) and `%n$s` + /// (positional) and returns a vector of `(text, param_index)`. The + /// `text` part is the literal substring, and `param_index` is the + /// 1‑based argument position (or 0 for trailing text). The default + /// implementation handles up to 8 positional parameters and any number + /// of sequential ones. + /// + /// You may override this if your translation format differs. fn split_translation(&self, text: String) -> Vec<(String, usize)> { let mut positions = vec![(0, 0, 0), (text.len(), 0, 0)]; for i in 1..=8 { @@ -39,13 +81,13 @@ pub trait TextResolutor { } } -impl TextResolutor for Arc { - fn resolve_content(&self, resolvable: &Resolvable) -> TextComponent { +impl<'a, T: TextResolutor<'a>> TextResolutor<'a> for Arc { + fn resolve_content(&self, resolvable: &Resolvable<'a>) -> RawTextComponent<'a> { (**self).resolve_content(resolvable) } #[cfg(feature = "custom")] - fn resolve_custom(&self, data: &CustomData) -> Option { + fn resolve_custom(&self, data: &CustomData<'a>) -> Option> { (**self).resolve_custom(data) } @@ -58,23 +100,29 @@ impl TextResolutor for Arc { } } +/// A `TextResolutor` that does no actual resolution. +/// +/// It returns stub text for scoreboards, entity selectors, NBT paths, and custom +/// data, and never translates any key. Useful for testing or when only the +/// static structure of a component is needed. pub struct NoResolutor; -impl TextResolutor for NoResolutor { - fn resolve_content(&self, resolvable: &Resolvable) -> TextComponent { + +impl<'a> TextResolutor<'a> for NoResolutor { + fn resolve_content(&self, resolvable: &Resolvable<'a>) -> RawTextComponent<'a> { match resolvable { Resolvable::Scoreboard { objective, .. } => { - TextComponent::plain(format!("[Score: {objective}]")) + RawTextComponent::plain(format!("[Score: {objective}]")) } Resolvable::Entity { selector, .. } => { - TextComponent::plain(format!("[Entity: {selector}]")) + RawTextComponent::plain(format!("[Entity: {selector}]")) } - Resolvable::NBT { path, .. } => TextComponent::plain(format!("[Nbt: {path}]")), + Resolvable::NBT { path, .. } => RawTextComponent::plain(format!("[Nbt: {path}]")), } } #[cfg(feature = "custom")] - fn resolve_custom(&self, data: &crate::custom::CustomData) -> Option { - Some(TextComponent::plain(data.id.clone())) + fn resolve_custom(&self, data: &crate::custom::CustomData<'a>) -> Option> { + Some(RawTextComponent::plain(data.id.clone())) } fn translate(&self, _key: &str) -> Option { @@ -82,8 +130,29 @@ impl TextResolutor for NoResolutor { } } -impl TextComponent { - pub fn build( +impl<'a> RawTextComponent<'a> { + /// Builds the component into a target format after full resolution. + /// + /// This method first resolves all dynamic content using the given `resolutor`, + /// then uses the `target` builder to convert the resolved component tree into + /// the desired output type (e.g., plain `String`, coloured terminal output, + /// NBT, JSON, etc.). + /// + /// # Example + /// ``` + /// # use text_components::RawTextComponent; + /// # use text_components::resolving::{TextResolutor, BuildTarget, NoResolutor}; + /// # struct MyTarget; + /// # impl<'a> BuildTarget<'a> for MyTarget { + /// # type Result = String; + /// # fn build_component + ?Sized>(&self, _: &R, c: &RawTextComponent<'a>) -> String { + /// # "built".to_string() + /// # } + /// # } + /// let component = RawTextComponent::plain("Hello"); + /// let result = component.build(&NoResolutor, MyTarget); + /// ``` + pub fn build + ?Sized, S: BuildTarget<'a>>( &self, resolutor: &R, target: S, @@ -91,12 +160,21 @@ impl TextComponent { target.build_component(resolutor, &self.resolve(resolutor)) } - pub fn resolve(&self, resolutor: &R) -> TextComponent { + /// Resolves all dynamic parts of the component recursively. + /// + /// This replaces `Resolvable` and `Custom` content with actual components + /// obtained from the `resolutor`, and also resolves arguments inside + /// `TranslatedMessage`, separators for entity/NBT resolvables, and children. + /// Formatting and interactivity are merged appropriately. + /// + /// The returned component is fully static (no more `Resolvable` leaves) + /// and can be serialized or built without further resolution. + pub fn resolve + ?Sized>(&self, resolutor: &R) -> RawTextComponent<'a> { let mut component = match &self.content { #[cfg(feature = "custom")] Content::Custom(data) => resolutor .resolve_custom(data) - .unwrap_or(TextComponent::new()), + .unwrap_or(RawTextComponent::new()), Content::Resolvable(resolvable) => resolutor.resolve_content(resolvable), content => resolutor.resolve_other(content), }; @@ -106,7 +184,7 @@ impl TextComponent { message.args = message.args.as_ref().map(|args| { args.iter() .map(|arg| arg.resolve(resolutor)) - .collect::>() + .collect::>() .into_boxed_slice() }); } @@ -133,11 +211,24 @@ impl TextComponent { } } -pub trait BuildTarget { +/// A target format for building a resolved text component. +/// +/// Implement this trait to convert a resolved `RawTextComponent` tree into +/// a concrete representation, such as a plain `String`, an ANSI‑coloured string, +/// an NBT tag, or a JSON value. +/// +/// # Type Parameter +/// * `Result` – The type produced by the builder (e.g., `String`, `NbtTag`). +pub trait BuildTarget<'a> { + /// The type produced by this builder. type Result; - fn build_component( + + /// Converts a single resolved component into the target representation. + /// + /// The method is called recursively for the whole component tree. + fn build_component + ?Sized>( &self, resolutor: &R, - component: &TextComponent, + component: &RawTextComponent<'a>, ) -> Self::Result; } diff --git a/src/translation.rs b/src/translation.rs index fa5a7b0..08d735f 100644 --- a/src/translation.rs +++ b/src/translation.rs @@ -1,28 +1,31 @@ -use crate::TextComponent; +use crate::RawTextComponent; use std::borrow::Cow; #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "databake", derive(::databake::Bake))] +#[cfg_attr(feature = "databake", databake(path = text_components::translation))] +#[cfg_attr(feature = "ownable", derive(::ownable::IntoOwned, ::ownable::ToOwned))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct TranslatedMessage { +pub struct TranslatedMessage<'a> { #[cfg_attr(feature = "serde", serde(rename = "translate"))] - pub key: Cow<'static, str>, + pub key: Cow<'a, str>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", default) )] - pub fallback: Option>, + pub fallback: Option>, #[cfg_attr( feature = "serde", serde(skip_serializing_if = "Option::is_none", rename = "with", default) )] - pub args: Option>, + pub args: Option]>>, } -impl TranslatedMessage { +impl<'a> TranslatedMessage<'a> { /// Creates a new `TranslatedMessage` without fallback. /// ### Warning /// Using this method directly is discouraged. /// Please use a compiled [Translation] instead. - pub const fn new(key: &'static str, args: Option>) -> Self { + pub const fn new(key: &'a str, args: Option]>>) -> Self { Self { key: Cow::Borrowed(key), args, @@ -31,42 +34,42 @@ impl TranslatedMessage { } #[inline] - pub fn component(self) -> TextComponent { - TextComponent::translated(self) + pub fn component(self) -> RawTextComponent<'a> { + RawTextComponent::translated(self) } #[inline] - pub fn component_fallback>>(mut self, fallback: F) -> TextComponent { + pub fn component_fallback(mut self, fallback: impl Into>) -> RawTextComponent<'a> { self.fallback = Some(fallback.into()); - TextComponent::translated(self) + RawTextComponent::translated(self) } } -impl From for TextComponent { - fn from(value: TranslatedMessage) -> Self { +impl<'a> From> for RawTextComponent<'a> { + fn from(value: TranslatedMessage<'a>) -> Self { value.component() } } -pub struct Translation(pub &'static str); +pub struct Translation<'a, const ARGS: usize>(pub &'a str); -impl Translation<0> { +impl<'a> Translation<'a, 0> { /// Creates a new `TranslatedMessage` with no arguments. #[must_use] - pub const fn msg(&self) -> TranslatedMessage { + pub const fn msg(&self) -> TranslatedMessage<'_> { TranslatedMessage::new(self.0, None) } } -impl Translation { +impl<'a, const ARGS: usize> Translation<'a, ARGS> { /// Creates a new `TranslatedMessage` with the given arguments. #[must_use] - pub fn message(&self, args: [impl Into; ARGS]) -> TranslatedMessage { + pub fn message(&self, args: [impl Into>; ARGS]) -> TranslatedMessage<'_> { TranslatedMessage::new(self.0, Some(Box::new(args.map(Into::into)))) } } -impl From<&Translation<0>> for TextComponent { - fn from(value: &Translation<0>) -> Self { +impl<'a> From<&'a Translation<'a, 0>> for RawTextComponent<'a> { + fn from(value: &'a Translation<'a, 0>) -> Self { value.msg().component() } }