This PR improves the changes made in #779.
Implementation: https://github.com/Spā¦ongePowered/SpongeCommon/pull/129
## Additions
- `Placeholder`s
- Placeholders can be used to calculate their replacement using the given context
- `Text.format(Text, Function<String,?>)`
- Allows lazy fetching/calulcation of replacements used in conjunction with `Placeholder`s
- Example: Directly fetch player prefixes from a permission plugin without being bound to just `prefix` or similar. `{world}{faction}{foobar}{prefix}{player}: {message}`
- Also avaiable as `Text.format(Text, Object...)` uses indexes as keys for the map
- Also avaiable as `Text.format(Text, Map<String,?>)` uses the function impl
- Similarize the behaviour of all `joinX(...)`methods
- `Text#joinOf(Text separator, Object... objects)`
- `Text#joinOf(Text separator, Iterable<?> contents)`
- `Text#formatBuilder()`
- Utility method to use the format call with named parameters in one row and without having to explicitly create a `Map` for it.
- `Functions` (Optional)
- Utility class for creating and wrapping `Function`s
- `BiFunctions` (Optional)
- Utility class for creating and wrapping `BiFunction`s
- `Dictionary` (Optional)
- Utility class for multi language support with fully configurable messages.
## Changes
- Better `TextRepresentable` specifications
- `Text.of(Object...)`
- added support for `Iterable`s
- added support for arrays
- Simplified handling of `TextRepresentable`s
## Test Plugin
[TextFormat.jar](https://dl.dropboxusercontent.com/u/16999313/Sponge/TextFormat.jar) (Plugin)
[TextFormat.zip](https://dl.dropboxusercontent.com/u/16999313/Sponge/TextFormat.zip) (Source)
Try the following commands to see the resutls:
- `/textformat`
- `/textformat PvPMonster`
- `/textformat PvPMonster Innocent`
- `/textformat PvPMonster Innocent &6Stick of Doom`
- `/textformat <YourUsername> Innocent` // Uses the item in hand
- `/texttype <CatalogType/*>`
- `/textjson` - Proves the json (de-)serialization works
- `/textxml` - Proves the xml(de-)serialization works
## Usage
Consider the following situation. You would like to show the following message to your arena participants on a regular basis.
```
Current ranking:
1) MonsterKiller - 42 points (324m away)
2) MunsterHunter - 21 points (123m away)
3) PlayerKiller - 5 points (2m away)
```
Without this PR you have basically four possibilities:
### 1) Using the `Text.Builder`
``` java
for (Player player : participants) {
player.sendMessage(Text.of("Current ranking:"));
int index = 1;
for (Player ranked : topRanked) {
Text.Builder builder = Text.builder();
builder.append(Text.of(index))
.append(Text.of(") "))
.append(Text.of(ranked))
.append(Text.of(" - "))
.append(Text.of(arena.getPoints(ranked)))
.append(Text.of(" points ("))
.append(Text.of(calcDistance(player, ranked)))
.append(Text.of("m away)"));
player.sendMessage(builder.build());
}
}
```
This is pretty verbose, lets reduce that a little bit.
### 2) Using `Text.of(Object...)`
``` java
for (Player player : participants) {
player.sendMessage(Text.of("Current ranking:"));
int index = 1;
for (Player ranked : topRanked) {
Text text = Text.of(index++, ") ", ranked, " - ", arena.getPoints(ranked), " points (", calcDistance(player, ranked), "m away)");
player.sendMessage(text);
}
}
```
This is shorter and easier to read, but you have to create the same `Text`s over and over again.
Also you cannot use different styles for different languages.
### 3) Using constants
``` java
Text MAIN_MESSAGE = Text.of("Current ranking:");
Text STATIC_PART1 = Text.of(") ");
Text STATIC_PART2 = Text.of(" - ");
Text STATIC_PART3 = Text.of(" points (");
Text STATIC_PART4 = Text.of("m away)");
for (Player player : participants) {
player.sendMessage(MAIN_MESSAGE);
int index = 1;
for (Player ranked : topRanked) {
Text text = Text.of(index++, STATIC_PART1, ranked, STATIC_PART2, arena.getPoints(ranked), STATIC_PART3, calcDistance(player, ranked), STATIC_PART4);
player.sendMessage(text);
}
}
```
Well, we don't create the messages over and over again, but this is quite horrible to read and you still cannot use different styles for different languages.
There must be something easier.
### NEW) Using `Text.format(Text, X)` and simple `Placeholder`s
``` java
Text MAIN_MESSAGE = Text.of("Current ranking:");
Text RANKING_MESSAGE = Text.of(Text.placeholder("0"), ") ", Text.placeholder("1"), " - ", Text.placeholder("2"), " points (", Text.placeholder("3"), "m away)");
for (Player player : participants) {
player.sendMessage(MAIN_MESSAGE);
int index = 1;
for (Player ranked : topRanked) {
player.sendMessage(Text.format(RANKING_MESSAGE, index++, ranked, arena.getPoints(ranked), calcDistance(player, ranked)));
}
}
```
Hell yeah, that looks pretty awesome. We also have multi language support now. We just have to exchange the `RANKING_MESSAGE`.
But wouldn't it be cool if I could just put the `points` and `distance` calculation inside the Text template? We should also replace the numeric placeholders with named onces to improve readability and simplify configuration.
### NEW + nice) Using `Text.format(Text, X)` and functional `Placeholder`s
Of course we can do that!
``` java
Text MAIN_MESSAGE = Text.of("Current ranking:");
Text RAW_RANKING_MESSAGE = Text.of(Text.placeholder("index"), ") ", Text.placeholder("ranked"), " - ", Text.placeholder("points"), " points (", Text.placeholder("distance"), "m away)");
// Prepare once (per arena)
Text RANKING_MESSAGE = Text.formatBuilder()
.with("points", Transformers.mappedKey("ranked". currentArena::getPoints))
.with("distance", Transformers.joinedKeys("target", "ranked", ArenaUtil::calcDistance))
.apply(RAW_RANKING_MESSAGE);
// Send to players infinite times
for (Player player : participants) {
player.sendMessage(MAIN_MESSAGE);
FormatBuilder formatter = Text.formatBuilder()
.with("index", new CountSupplier(0))
.with("target", player);
for (Player ranked : topRanked) {
player.sendMessage(formatter.with("ranked", ranked).apply(RANKING_MESSAGE));
}
}
```
Yes this looks very straight forward and i probably won't ever have to change the `sendMessage` lines ever again. Only the `Text` message itself and possibly the functions that do the customizations.
Mhh, there is the preparation step that requires me to have the arena there, but I would like to store the template as static field, isn't it possible to provide the arena later as well?
Sure thats possible:
``` java
// Prepare once (for all arenas)
static final Text RANKING_MESSAGE = Text.formatBuilder()
.with("points", Transformers.joinedKeys("arena", "ranked", Arena::getPoints))
.with("distance", Transformers.joinedKeys("target", "ranked", ArenaUtil::calcDistance))
.apply(RAW_RANKING_MESSAGE);
// Send to players infinite times
for (Player player : participants) {
player.sendMessage(MAIN_MESSAGE);
FormatBuilder formatter = Text.formatBuilder()
.with("index", new CountSupplier(1))
.with("arena", currentArena)
.with("target", player);
for (Player ranked : topRanked) {
player.sendMessage(formatter.with("ranked", ranked).apply(RANKING_MESSAGE));
}
}
```
And I can still replace the `Text` instances if I want to use a different language, but I don't want to create a switch for multiple languages in the code, isn't it possible to push that to the `Placeholder` as well?
### NEW + FANCY) Using `Text.format(Text, X)` and multilingual `Placeholder`s
**Declaration:**
``` java
// DictionaryHolder is a plugin private wrapper class statically holding a Dictionary instance
static final Text MAIN_MESSAGE = DictionaryHolder.getWithTarget("MAIN_MESSAGE", "target")
.asPlaceholder();
static final Text RANKING_MESSAGE = DictionaryHolder.prepare("RANKING_MESSAGE")
.with("points", Transformers.joinedKeys("arena", "ranked", Arena::getPoints))
.with("distance", Transformers.joinedKeys("target", "ranked", ArenaUtil::calcDistance))
.doneWithTarget("target")
.asPlaceholder();
```
**Usage:**
``` java
// Send to players infinite times
for (Player player : participants) {
FormatBuilder formatter = Text.formatBuilder()
.with("index", new CountSupplier(1))
.with("arena", currentArena)
.with("target", player);
player.sendMessage(formatter.apply(MAIN_MESSAGE));
for (Player ranked : topRanked) {
player.sendMessage(formatter.with("ranked", ranked).apply(RANKING_MESSAGE));
}
}
```
See here for an example [`DictionaryHolder`](https://gist.github.com/ST-DDT/3565623155e1dc64568f) implementation. This is (except from loading the template `Text`s from a file) the only thing plugin developers have to "implement themselves" to use full multi language support in their plugins.
With these easy changes we have FULL multi language support and we can concentrate on developing our plugin's features and only have to maintain the translation/text files. (And since the translation files are just plain texts even none-developers can create and update them.)
### Configurable prefixes
You want to allow your plugin users/server owner to configure whether you show the player's prefix in the ranking list? Thats quite easy.
``` java
DictionaryHolder.prepare("X")
.with(key -> key.startsWith("prefix@"), key -> {
return Transformers.flatMappedKey(key.substring(7), subject -> ((OptionSubject) subject).getOption("prefix"))
.orEmptyText()
.asPlaceholder();
})
[...]
```
Template: `Hi ${prefix@target} ${target}!`
As you can see this setup is not bound to any instance in particular, so you can reuse it across your entire plugin without changes.
Please also note that this only affects the declaration section and will work without any changes to our usage section and without changing the API's default Text parser. And yes you still have multi language support and thus you could show the prefix only to `PIRATE_ENGLISH` users for example (and it won't be calculated it if you don't use it).
Well, you aren't sure whether there will be a permission plugin that supports `OptionSubject` and don't trust the server admin to configure your messages properly (or they are configured that way by default). And you don't want a `ClassCastException` to pop up?
``` java
DictionaryHolder.prepare("X")
.with(key -> key.startsWith("prefix@"), key -> {
return Transformers.flatMappedKey(key.substring(7), Functions.conditional(subject -> subject instanceof OptionSubject,
subject -> ((OptionSubject) subject).getOption("prefix"), Functions.constantNull()))
.orEmptyText()
.asPlaceholder();
})
[...]
```
Template: `Hi ${prefix@target} ${target}!`
If I can support prefixes, why not support everything?
``` java
DictionaryHolder.prepare("X")
.with(key -> key.contains("@SUBJECT@"), key -> {
String[] split = key.split("@SUBJECT@", 2);
return Transformers.flatMappedKey(split[0], Functions.conditional(subject -> subject instanceof OptionSubject,
subject -> ((OptionSubject) subject).getOption(split[1]), Functions.constantNull()))
.orEmptyText()
.asPlaceholder();
})
[...]
```
Template: `Hi ${target@SUBJECT@prefix}${target@SUBJECT@title} ${target} ${target@SUBJECT@suffix}!`
The format is up to you!
PS: I recommend that such functionalities should be centralized by plugin developers (maybe even pushed inside their `DictionaryHolder`).
---
**Any questions left?**
---
**Other Suggestions**
(Not part of this PR)
@zml2008
[MessageEventSuggestion ](https://gist.github.com/ST-DDT/3b337cfa42b1a621e957)
or with the latest additions you will probably add a `FormatBuilder` or a context `Function` to the `MessageEvent` or its associated `MessageSink`