Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 115 additions & 44 deletions examples/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::{borrow::Cow, collections::HashMap, sync::Arc};

#[cfg(feature = "custom")]
use chrono::Utc;
use chrono::Local;
#[cfg(feature = "serde")]
use serde::Serialize;
#[cfg(feature = "nbt")]
Expand All @@ -8,7 +10,7 @@ use simdnbt::{
owned::{BaseNbt, Nbt, NbtCompound, NbtTag},
};
#[cfg(feature = "custom")]
use text_components::custom::{CustomContent, CustomData, CustomRegistry, Payload};
use text_components::custom::{CustomContent, CustomContentExt, CustomData, Payload};
#[cfg(feature = "nbt")]
use text_components::nbt::{NbtBuilder, ToSNBT};
use text_components::{
Expand All @@ -17,27 +19,98 @@ use text_components::{
fmt::set_display_resolutor,
format::Color,
interactivity::{ClickEvent, HoverEvent},
resolving::TextResolutor,
translation::{TranslatedMessage, Translation},
resolving::{ResolutionHelper, TextResolutor, set_resolution_helper},
translation::{TranslatedContent, Translation, TranslationToken},
};
use uuid::Uuid;

struct EmptyResolutor;
impl<'a> TextResolutor<'a> for EmptyResolutor {
fn translate(&self, key: &str) -> Option<String> {
match key {
"content" => Some(String::from(
"This is a test TextComponent!\n Color: %s\n Bold: %s\n Italic: %s\n Underline: %s\n Strikethrough: %s\n Obfuscated: %s\n Shadow Color: %s\n Translation: %s\n Link: %s\n(All the green text is translated with arguments checked at compile time!)",
)),
"translated" => Some(String::from(
struct GlobalHelper {
pub translations: HashMap<&'static str, &'static [TranslationToken<'static>]>,
}
impl GlobalHelper {
fn new() -> Self {
let mut this = Self {
translations: HashMap::new(),
};
this.register(
"en",
"content",
&[
TranslationToken::Text(Cow::Borrowed("This is a test TextComponent!\n Color: ")),
TranslationToken::Arg(1),
TranslationToken::Text(Cow::Borrowed("\n Bold: ")),
TranslationToken::Arg(2),
TranslationToken::Text(Cow::Borrowed("\n Italic: ")),
TranslationToken::Arg(3),
TranslationToken::Text(Cow::Borrowed("\n Underline: ")),
TranslationToken::Arg(4),
TranslationToken::Text(Cow::Borrowed("\n Strikethrough: ")),
TranslationToken::Arg(5),
TranslationToken::Text(Cow::Borrowed("\n Obfuscated: ")),
TranslationToken::Arg(6),
TranslationToken::Text(Cow::Borrowed("\n Shadow Color: ")),
TranslationToken::Arg(7),
TranslationToken::Text(Cow::Borrowed("\n Translation: ")),
TranslationToken::Arg(8),
TranslationToken::Text(Cow::Borrowed("\n Link: ")),
TranslationToken::Arg(9),
TranslationToken::Text(Cow::Borrowed(
"\n(All the green text is translated with arguments checked at compile time!)",
)),
],
);
this.register(
"en",
"translated",
&[TranslationToken::Text(Cow::Borrowed(
"This text is Translated! (Without compile time check!)",
)),
"resoluble" => Some(String::from(
"\n\nResolubles:\n Object: %s\n Scoreboard: %s\n Entity: %s\n Nbt: %s",
)),
_ => None,
))],
);
this.register(
"en",
"resoluble",
&[
TranslationToken::Text(Cow::Borrowed("\n\nResolubles:\n Object: ")),
TranslationToken::Arg(1),
TranslationToken::Text(Cow::Borrowed("\n Scoreboard: ")),
TranslationToken::Arg(2),
TranslationToken::Text(Cow::Borrowed("\n Entity: ")),
TranslationToken::Arg(3),
TranslationToken::Text(Cow::Borrowed("\n Nbt: ")),
TranslationToken::Arg(4),
],
);
this
}

fn register(
&mut self,
_locale: &'static str,
key: &'static str,
translation: &'static [TranslationToken<'static>],
) {
self.translations.insert(key, translation);
}
}
impl<'a> ResolutionHelper<'a> for GlobalHelper {
#[cfg(feature = "custom")]
fn resolve_custom(
&self,
resolutor: &dyn TextResolutor<'a>,
data: &CustomData,
) -> Option<RawTextComponent<'a>> {
if data.id == "time" {
return Some(TimeContent.resolve(resolutor, (), Payload::Empty));
}
None
}
fn translate(&self, _locale: &str, key: &str) -> Option<&[TranslationToken<'a>]> {
self.translations.get(key).copied()
}
}

struct EmptyResolutor;
impl<'a> TextResolutor<'a> for EmptyResolutor {
fn resolve_content(&self, resolvable: &Resolvable) -> RawTextComponent<'a> {
match resolvable {
Resolvable::Scoreboard { .. } => RawTextComponent::plain("5"),
Expand Down Expand Up @@ -69,25 +142,6 @@ impl<'a> TextResolutor<'a> for EmptyResolutor {
}
}
}
#[cfg(feature = "custom")]
fn resolve_custom(&self, data: &CustomData) -> Option<RawTextComponent<'a>> {
if data.id == "time" {
return Some(TimeContent.resolve((), Payload::Empty));
}
None
}
}
#[cfg(feature = "custom")]
impl<'a> CustomRegistry<'a> for EmptyResolutor {
type Data = ();

fn register_content<T: CustomContent<'a>>(&mut self, _id: &'a str, _content: T) {
todo!()
}

fn get_content(&self, _id: String) -> Box<dyn CustomContent<'_, Reg = Self>> {
Box::new(TimeContent)
}
}

const CONTENT: Translation<9> = Translation("content");
Expand All @@ -96,23 +150,40 @@ const RESOLUBLE: Translation<4> = Translation("resoluble");
#[cfg(feature = "custom")]
struct TimeContent;
#[cfg(feature = "custom")]
impl<'a> CustomContent<'a> for TimeContent {
type Reg = EmptyResolutor;

impl<'a> CustomContentExt<'a> for TimeContent {
fn as_data(&self) -> CustomData<'a> {
CustomData {
id: std::borrow::Cow::Borrowed("time"),
payload: Payload::Empty,
}
}

fn resolve(&self, _data: (), _payload: Payload) -> RawTextComponent<'a> {
RawTextComponent::plain(Utc::now().format("%H:%M").to_string())
}
#[cfg(feature = "custom")]
impl<'a> CustomContent<'a, ()> for TimeContent {
fn resolve(
&self,
_resolutor: &dyn TextResolutor<'a>,
_context: (),
_payload: Payload,
) -> RawTextComponent<'a> {
RawTextComponent::plain(Local::now().format("%H:%M").to_string())
}
}
#[cfg(feature = "custom")]
impl<'a> CustomContent<'a, i8> for TimeContent {
fn resolve(
&self,
_resolutor: &dyn TextResolutor<'a>,
_context: i8,
_payload: Payload,
) -> RawTextComponent<'a> {
RawTextComponent::plain(Local::now().format("%H:%M").to_string())
}
}

fn main() {
set_display_resolutor(&EmptyResolutor);
set_resolution_helper(Arc::new(GlobalHelper::new()));
let resolubles = RESOLUBLE
.message([
ObjectPlayer::name("MrMelther").reset(),
Expand All @@ -133,7 +204,7 @@ fn main() {
"This text is ShadowcoloRED!"
.reset()
.shadow_color(255, 128, 0, 0),
TranslatedMessage::new("translated", None).reset(),
TranslatedContent::new("translated", None).reset(),
"This text contains a link!"
.click_event(ClickEvent::open_url(
"https://github.com/Steel-Foundation/TextComponents",
Expand All @@ -148,7 +219,7 @@ fn main() {
component.add_child(resolubles.add_children(vec!["\n Custom: ".into(), TimeContent.reset()]))
}
_ => {
component
component.add_child(resolubles)
}
};

Expand Down
4 changes: 2 additions & 2 deletions examples/serde.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use text_components::{Modifier, RawTextComponent, format::Color, translation::TranslatedMessage};
use text_components::{Modifier, RawTextComponent, format::Color, translation::TranslatedContent};

fn main() {
let component: RawTextComponent = TranslatedMessage::new("key", None)
let component: RawTextComponent = TranslatedContent::new("key", None)
.color(Color::Blue)
.bold(true);
println!("{}", serde_json::to_string_pretty(&component).unwrap());
Expand Down
32 changes: 32 additions & 0 deletions examples/snbt.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
use text_components::RawTextComponent;

#[cfg(feature = "custom")]
mod custom {
use text_components::{
RawTextComponent,
custom::CustomData,
resolving::{ResolutionHelper, TextResolutor},
translation::TranslationToken,
};

pub struct GlobalHelper;
impl<'a> ResolutionHelper<'a> for GlobalHelper {
fn resolve_custom(
&self,
_resolutor: &dyn TextResolutor<'a>,
data: &CustomData<'a>,
) -> Option<RawTextComponent<'a>> {
match data.id.as_ref() {
"hello" => Some(RawTextComponent::const_plain("Hello World!")),
_ => None,
}
}

fn translate(&self, _locale: &str, _key: &str) -> Option<&[TranslationToken<'a>]> {
None
}
}
}

fn main() {
use std::io::{Write, stdin, stdout};
#[cfg(feature = "custom")]
text_components::resolving::set_resolution_helper(std::sync::Arc::new(custom::GlobalHelper));

let mut s = String::new();
print!("/tellraw @s ");
let _ = stdout().flush();
Expand All @@ -25,4 +56,5 @@ fn main() {
// ["\"Howdy!\"", { text:"\nThis is a text component!\n", color:'blue', "bold":1b, italic:true }, {text:"Texto" , type: "translatable", fallback:"lol\n", translate:"lmao"}, {sprite:"items/iron_sword"}, "\n", {object:"player", player:{name:"MrMelther"}, hover_event:{action:"show_text",value:{text:"Send msg to MrMelther"}}, click_event:{action:"suggest_command", command:"/msg MrMelther "} }, {object:"player", player:{properties:[{name:"textures", value:"[Put your base64 texture here!]"}]}}]
// {text:"Test",extra:[" lmao"]}
// "Workflow test!"
// {custom:{id:"hello"}, color:"green"}
}
65 changes: 64 additions & 1 deletion src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use quote::quote;
use serde_json::Value;
use std::fs;

use crate::translation::TranslationToken;

/// Count the number of parameters in a translation string
fn count_parameters(text: &str) -> usize {
let sequential = text.matches("%s").count();
Expand All @@ -16,6 +18,40 @@ fn count_parameters(text: &str) -> usize {
sequential.max(positional)
}

fn process_tokens(text: &str) -> (TokenStream, i32) {
let mut positions = vec![(0, 0, 0), (text.len(), 0, 0)];
for i in 1..=8 {
for (pos, _) in text.match_indices(&format!("%{i}$s")) {
positions.push((pos, i, 4usize));
}
}
for (counter, (pos, _)) in (1..).zip(text.match_indices("%s")) {
positions.push((pos, counter, 2usize));
}
positions.sort_by_key(|(pos, _, _)| *pos);
let mut positions = positions.into_iter().peekable();
let mut stream = TokenStream::new();
let mut amount = 0;
while let Some((pos, _, size)) = positions.next() {
let Some(next) = positions.peek() else {
break;
};
let text = text[pos + size..next.0].to_string();
let arg = next.1;
stream.extend(quote! {
TranslationToken::Text(Cow::borrow(#text)),
});
amount += 1;
if arg > 0 {
stream.extend(quote! {
TranslationToken::Arg(#arg),
});
amount += 1;
}
}
(quote! {[#stream]}, amount)
}

pub fn build_translations(path: &str) -> TokenStream {
println!("cargo:rerun-if-changed={path}");

Expand All @@ -30,7 +66,11 @@ pub fn build_translations(path: &str) -> TokenStream {
// Add imports
stream.extend(quote! {
#![allow(dead_code)]
use text_components::translation::Translation;
use text_components::{
translation::{Translation, TranslationToken},
build::TranslationsRegistry
};
use std::borrow::Cow;
});

// Generate constants for each translation
Expand All @@ -39,6 +79,7 @@ pub fn build_translations(path: &str) -> TokenStream {

// Track used constant names to handle collisions
let mut used_names = rustc_hash::FxHashMap::default();
let mut register_stream = TokenStream::new();

for (key, value) in translations_vec {
let Some(text) = value.as_str() else {
Expand Down Expand Up @@ -67,12 +108,34 @@ pub fn build_translations(path: &str) -> TokenStream {
}

let const_name = Ident::new(&const_name_str, Span::call_site());
let (translation_tokens, tokens_amount) = process_tokens(text);
let token_const_name = Ident::new(&format!("{const_name_str}_TOKEN"), Span::call_site());

stream.extend(quote! {
#[doc = #text]
pub static #const_name: Translation<#param_count> = Translation(#key);
static #token_const_name: [TranslationToken<'static>; #tokens_amount] = #translation_tokens;
});

register_stream.extend(quote! {
registry.register("en", #key, &#token_const_name);
});
}

stream.extend(quote! {
fn register_translations<R: TranslationsRegistry>(registry: &mut R) {
#register_stream
}
});

stream
}

pub trait TranslationsRegistry {
fn register(
&mut self,
locale: &'static str,
key: &'static str,
translation: &'static [TranslationToken<'static>],
);
}
Loading
Loading