Browse Source

Add `TextTemplate` class

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
47a4095bb1
  1. 29
      sources/Manifest.uc
  2. 361
      sources/Text/Tests/TEST_TextTemplate.uc
  3. 87
      sources/Text/TextAPI.uc
  4. 975
      sources/Text/TextTemplate.uc

29
sources/Manifest.uc

@ -42,18 +42,19 @@ defaultproperties
testCases(11) = class'TEST_JSON' testCases(11) = class'TEST_JSON'
testCases(12) = class'TEST_TextCache' testCases(12) = class'TEST_TextCache'
testCases(13) = class'TEST_FormattedStrings' testCases(13) = class'TEST_FormattedStrings'
testCases(14) = class'TEST_User' testCases(14) = class'TEST_TextTemplate'
testCases(15) = class'TEST_Memory' testCases(15) = class'TEST_User'
testCases(16) = class'TEST_DynamicArray' testCases(16) = class'TEST_Memory'
testCases(17) = class'TEST_AssociativeArray' testCases(17) = class'TEST_DynamicArray'
testCases(18) = class'TEST_CollectionsMixed' testCases(18) = class'TEST_AssociativeArray'
testCases(19) = class'TEST_Iterator' testCases(19) = class'TEST_CollectionsMixed'
testCases(20) = class'TEST_Command' testCases(20) = class'TEST_Iterator'
testCases(21) = class'TEST_CommandDataBuilder' testCases(21) = class'TEST_Command'
testCases(22) = class'TEST_LogMessage' testCases(22) = class'TEST_CommandDataBuilder'
testCases(23) = class'TEST_DatabaseCommon' testCases(23) = class'TEST_LogMessage'
testCases(24) = class'TEST_LocalDatabase' testCases(24) = class'TEST_DatabaseCommon'
testCases(25) = class'TEST_AcediaConfig' testCases(25) = class'TEST_LocalDatabase'
testCases(26) = class'TEST_UTF8EncoderDecoder' testCases(26) = class'TEST_AcediaConfig'
testCases(27) = class'TEST_AvariceStreamReader' testCases(27) = class'TEST_UTF8EncoderDecoder'
testCases(28) = class'TEST_AvariceStreamReader'
} }

361
sources/Text/Tests/TEST_TextTemplate.uc

@ -0,0 +1,361 @@
/**
* Set of tests for functionality of `TextTemplate`.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_TextTemplate extends TestCase
abstract;
protected static function TESTS()
{
Context("Testing `TextTemplate` class' ability to handle empty (erroneous)"
@ "argument declarations.");
Test_EmptyDeclarations();
Context("Testing `TextTemplate` class' ability to handle numeric"
@ "argument declarations.");
Test_NumericDeclarationsAmount();
Test_NumericDeclarationsOrder();
Test_NumericDeclarationsNoOrder();
Test_NumericDeclarationsRandomValues();
Context("Testing `TextTemplate` class' ability to handle text"
@ "argument declarations.");
Test_TextDeclarationsGet();
Test_TextDeclarationsEmptyCollect();
Test_TextDeclarationsCollect();
Test_TextDeclarationsOverwriteCollect();
Context("Testing complex `TextTemplate` scenarios.");
Test_Reset();
Test_Complex();
Test_Formatted();
}
protected static function Test_EmptyDeclarations()
{
local TextTemplate instance;
Issue("Empty `TextTemplate` is not collected into empty text.");
instance = __().text.MakeTemplate_S("");
TEST_ExpectTrue(instance.Collect().IsEmpty());
Issue("`TextTemplate` with empty numeric definition is not properly"
@ "collected.");
instance = __().text.MakeTemplate_S("%");
TEST_ExpectTrue(instance.Collect().IsEmpty());
instance = __().text.MakeTemplate_S("With some%text");
TEST_ExpectTrue(instance.Collect().ToString() == "With sometext");
Issue("`TextTemplate` with empty text definition is not properly"
@ "collected.");
instance = __().text.MakeTemplate_S("%%");
TEST_ExpectTrue(instance.Collect().IsEmpty());
instance = __().text.MakeTemplate_S("With some%%%text and %%%%more!");
TEST_ExpectTrue(instance.Collect().ToString() == "With sometext and more!");
Issue("`TextTemplate` with definitions opened at the very end is not"
@ "properly collected.");
instance = __().text.MakeTemplate_S("Some text: %");
TEST_ExpectTrue(instance.Collect().ToString() == "Some text: ");
instance = __().text.MakeTemplate_S("Another example: %%");
TEST_ExpectTrue(instance.Collect().ToString() == "Another example: ");
}
protected static function Test_NumericDeclarationsAmount()
{
local TextTemplate instance;
Issue("`GetNumericArgsAmount()` does not return `0` for `Text` without any"
@ "numeric declarations.");
instance = __().text.MakeTemplate_S("");
TEST_ExpectTrue(instance.GetNumericArgsAmount() == 0);
instance = __().text.MakeTemplate_S("With some%text");
TEST_ExpectTrue(instance.GetNumericArgsAmount() == 0);
instance = __().text.MakeTemplate_S("With some%%%text and %%%%more!");
TEST_ExpectTrue(instance.GetNumericArgsAmount() == 0);
instance = __().text.MakeTemplate_S("With some%%valid arg%text and"
@ "%%me too%%more!");
TEST_ExpectTrue(instance.GetNumericArgsAmount() == 0);
Issue("`GetNumericArgsAmount()` does not correctly counts numeric"
@ "declarations.");
instance = __().text.MakeTemplate_S("just %1 %2 %3 %t %4 %5 %%text one!%%"
@" %6 %7 %8");
TEST_ExpectTrue(instance.GetNumericArgsAmount() == 8);
Issue("`GetNumericArgsAmount()` does not correctly counts numeric"
@ "declarations with escaped '%' characters.");
instance = __().text.MakeTemplate_S("just &%1 %2 %3 %t &%4 %5 %%text one!%%"
@" %6 %7 %8");
TEST_ExpectTrue(instance.GetNumericArgsAmount() == 6);
}
protected static function Test_NumericDeclarationsOrder()
{
local TextTemplate instance;
Issue("Basic numeric declarations are not working as intended.");
instance = __().text.MakeTemplate_S("This is argument: %1!");
instance.Arg(P("<enter_phrase_here>"));
TEST_ExpectTrue(instance.Collect().ToString()
== "This is argument: <enter_phrase_here>!");
instance = __().text.MakeTemplate_S("just %1 %2 %3 %4 %5 %6 %7 %8");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c")).Arg(P("d")).Arg(P("e"))
.Arg(P("f")).Arg(P("g")).Arg(P("h"));
TEST_ExpectTrue(instance.Collect().ToString()
== "just a b c d e f g h");
Issue("Basic numeric declarations are not working as intended after"
@ "specifying too many.");
instance = __().text.MakeTemplate_S("This is argument: %1!");
instance.Arg(P("<enter_phrase_here>")).Arg(P("Now this is too much!"));
TEST_ExpectTrue(instance.Collect().ToString()
== "This is argument: <enter_phrase_here>!");
instance = __().text.MakeTemplate_S("just %1 %2 %3 %4 %5 %6 %7 %8");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c")).Arg(P("d")).Arg(P("e"))
.Arg(P("f")).Arg(P("g")).Arg(P("h")).Arg(P("i")).Arg(P("j"));
TEST_ExpectTrue(instance.Collect().ToString()
== "just a b c d e f g h");
Issue("Basic numeric declarations are not working as intended after"
@ "specifying too little.");
instance = __().text.MakeTemplate_S("This is argument: %1!");
TEST_ExpectTrue(instance.Collect().ToString() == "This is argument: !");
instance = __().text.MakeTemplate_S("just %1 %2 %3 %4 %5 %6 %7 %8");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c")).Arg(P("d")).Arg(P("e"));
TEST_ExpectTrue(instance.Collect().ToString() == "just a b c d e ");
}
protected static function Test_NumericDeclarationsNoOrder()
{
local TextTemplate instance;
Issue("Basic numeric declarations are not working as intended.");
instance = __().text.MakeTemplate_S("This is argument: %2! And this: %1!!");
instance.Arg(P("<enter_phrase_here>")).Arg(P("heh"));
TEST_ExpectTrue(instance.Collect().ToString()
== "This is argument: heh! And this: <enter_phrase_here>!!");
instance = __().text.MakeTemplate_S("just %1 %3 %5 %2 %4 %8 %6 %7");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c")).Arg(P("d")).Arg(P("e"))
.Arg(P("f")).Arg(P("g")).Arg(P("h"));
TEST_ExpectTrue(instance.Collect().ToString()
== "just a c e b d h f g");
Issue("Basic numeric declarations are not working as intended after"
@ "specifying too many.");
instance = __().text.MakeTemplate_S("just %1 %3 %5 %2 %4 %8 %6 %7");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c")).Arg(P("d")).Arg(P("e"))
.Arg(P("f")).Arg(P("g")).Arg(P("h")).Arg(P("i")).Arg(P("j"));
TEST_ExpectTrue(instance.Collect().ToString()
== "just a c e b d h f g");
Issue("Basic numeric declarations are not working as intended after"
@ "specifying too little.");
instance = __().text.MakeTemplate_S("just %1 %3 %5 %2 %4 %8 %6 %7");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c"));
TEST_ExpectTrue(instance.Collect().ToString() == "just a c b ");
}
protected static function Test_NumericDeclarationsRandomValues()
{
local TextTemplate instance;
Issue("Basic numeric declarations are not working as intended when using"
@ "arbitrary integer values.");
instance = __().text.MakeTemplate_S("This is argument:"
@ "%205! And this: %-7!!");
instance.Arg(P("<enter_phrase_here>")).Arg(P("heh"));
TEST_ExpectTrue(instance.Collect().ToString()
== "This is argument: heh! And this: <enter_phrase_here>!!");
instance = __().text.MakeTemplate_S("just %-3912842 %0 %666 %-111 %34"
@ "%666999 %10234 %10235");
instance.Arg(P("a")).Arg(P("b")).Arg(P("c")).Arg(P("d")).Arg(P("e"))
.Arg(P("f")).Arg(P("g")).Arg(P("h"));
TEST_ExpectTrue(instance.Collect().ToString()
== "just a c e b d h f g");
}
protected static function Test_TextDeclarationsGet()
{
local TextTemplate instance;
local array<Text> result;
Issue("Specified text labels aren't properly returned by"
@ "the `GetTextlabels()` method.");
instance = __().text.MakeTemplate_S("This is argument: %%argument!%% and"
@ "so its this: %%another arg%%");
result = instance.GetTextArgs();
TEST_ExpectTrue(result[0].ToString() == "argument!");
TEST_ExpectTrue(result[1].ToString() == "another arg");
TEST_ExpectTrue(result.length == 2);
instance.TextArg(P("another arg"), P("heh"))
.TextArg(P("argument!"), P("<enter_phrase_here>"));
result = instance.GetTextArgs();
TEST_ExpectTrue(result[0].ToString() == "argument!");
TEST_ExpectTrue(result[1].ToString() == "another arg");
TEST_ExpectTrue(result.length == 2);
instance = __().text.MakeTemplate_S("Here: %2 %76 %4 %5 %8");
result = instance.GetTextArgs();
TEST_ExpectTrue(result.length == 0);
instance = __().text.MakeTemplate_S("More %%complex%% %7 %%/example% here,"
@ "yes. Very %%complex%!");
result = instance.GetTextArgs();
TEST_ExpectTrue(result[0].ToString() == "complex");
TEST_ExpectTrue(result[1].ToString() == "/example");
TEST_ExpectTrue(result.length == 2);
}
protected static function Test_TextDeclarationsEmptyCollect()
{
local TextTemplate instance;
Issue("`TextTempalte` is not properly collected with it contains empty"
@ "text labels.");
instance = __().text.MakeTemplate_S("This is argument: %%%% and"
@ "so its this: %%%");
instance.TextArg(P(""), P("what do y'll know?!"));
TEST_ExpectTrue(instance.Collect().ToString()
== ("This is argument: what do y'll know?! and so its this:"
@ "what do y'll know?!"));
}
protected static function Test_TextDeclarationsCollect()
{
local TextTemplate instance;
Issue("Specified text labels aren't properly collected.");
instance = __().text.MakeTemplate_S("This is argument: %%argument!%% and"
@ "so its this: %%another arg%%");
instance.TextArg(P("another arg"), P("heh"))
.TextArg(P("argument!"), P("<enter_phrase_here>"));
TEST_ExpectTrue(instance.Collect().ToString()
== "This is argument: <enter_phrase_here> and so its this: heh");
Issue("Specified text labels aren't properly collected when duplicate"
@ "labels are present.");
instance = __().text.MakeTemplate_S("More %%complex%% %7 %%/example% here,"
@ "yes. Very %%complex%!");
instance.TextArg(P("complex"), P("simple")).TextArg(P("/example"), P("!!"));
TEST_ExpectTrue(instance.Collect().ToString()
== "More simple !! here, yes. Very simple!");
Issue("Specified text labels aren't properly collected when specified"
@ "too little arguments.");
instance = __().text.MakeTemplate_S("More %%complex%% %7 %%/example% here,"
@ "yes. Very %%complex% and %%nasty%%!!!");
instance.TextArg(P("complex"), P("simple")).TextArg(P("nasty"), P("-_-"));
TEST_ExpectTrue(instance.Collect().ToString()
== "More simple here, yes. Very simple and -_-!!!");
Issue("Specified text labels aren't properly collected when specified"
@ "too many arguments.");
instance = __().text.MakeTemplate_S("More %%complex%% %11 %%/example% here,"
@ "yes. Very %%complex% and %%nasty%%!!!");
instance.TextArg(P("complex"), P("simple")).TextArg(P("nasty"), P("-_-"))
.TextArg(P("/usr/bin"), P("geronimo")).TextArg(P("/example"), P("???"));
TEST_ExpectTrue(instance.Collect().ToString()
== "More simple ??? here, yes. Very simple and -_-!!!");
}
protected static function Test_TextDeclarationsOverwriteCollect()
{
local TextTemplate instance;
Issue("Specified text labels aren't properly collected when some labels"
@ "are overwritten.");
instance = __().text.MakeTemplate_S("More %%complex%% %7 %%/example% here,"
@ "yes. Very %%complex%!");
instance.TextArg(P("complex"), P("simple")).TextArg(P("/example"), P("!!"))
.TextArg(P("complex"), P("nasty")).TextArg(P("/exe"), P("???"));
TEST_ExpectTrue(instance.Collect().ToString()
== "More nasty !! here, yes. Very nasty!");
}
protected static function Test_Reset()
{
local TextTemplate instance;
Issue("`Reset()` does not properly reset user's input.");
instance = __().text.MakeTemplate_S("Testing %1, %2, %3 and %%one%%,"
@ "%%two%% + %%three%%");
instance.Arg_S("1").Arg_S("2").Arg_S("3").Arg_S("more?");
instance
.TextArg(P("one"), P("4"))
.TextArg(P("two"), P("5"))
.TextArg(P("three"), P("6"));
instance.Reset().Arg_S("HEY").Arg_S("ARE").Arg_S("YOU");
instance
.TextArg(P("one"), P("READY"))
.TextArg(P("two"), P("TO"))
.TextArg(P("three"), P("GO?"));
TEST_ExpectTrue(instance.Collect().ToString() == ("Testing HEY, ARE, YOU"
@ "and READY, TO + GO?"));
}
protected static function Test_Complex()
{
local TextTemplate instance;
local array<Text> result;
Issue("Specified text labels aren't properly collected in complex scenario"
@ "with several numeric / text arguments and escaped characters.");
instance = __().text.MakeTemplate_S("Welcome %%MoonAndStar%%, it is %7 nice"
@ "to %-2 %%you%%. %%MoonAndStar%% drop your %%weapons%%, it is not too"
@ "%0 for &m&y %%mercy%&%!");
instance
.TextArg(P("MoonAndStar"), P("Nerevar"))
.TextArg(P("you"), P("you"))
.TextArg(P("weapons"), P("cheats"))
.Arg(P("see"))
.Arg(P("late"))
.Arg(P("so"));
TEST_ExpectTrue(instance.Collect().ToString()
== ("Welcome Nerevar, it is so nice to see you."
@ "Nerevar drop your cheats, it is not too late for &m&y %!"));
result = instance.GetTextArgs();
TEST_ExpectTrue(result[0].ToString() == "MoonAndStar");
TEST_ExpectTrue(result[1].ToString() == "you");
TEST_ExpectTrue(result[2].ToString() == "weapons");
TEST_ExpectTrue(result[3].ToString() == "mercy");
TEST_ExpectTrue(result.length == 4);
}
protected static function Test_Formatted()
{
local TextTemplate instance;
Issue("Specified text labels aren't properly collected in complex scenario"
@ "with several numeric / text arguments and escaped characters and"
@ "we are asking to parse template as a formatted string"
@ "(`CollectFormatted()` method).");
instance = __().text.MakeTemplate_S("Test simple {%%color%% %1} string"
@ "that is {%2 %%what%%}!");
instance
.Arg(P("formatted"))
.Arg(P("$blue"))
.TextArg(P("color"), P("$red"))
.TextArg(P("what"), P("colored"));
TEST_ExpectTrue(instance.CollectFormatted().ToFormattedString()
== ("Test simple {rgb(255,0,0) formatted} string that is"
@ "{rgb(0,0,255) colored}!"));
}
defaultproperties
{
caseName = "TextTemplate"
caseGroup = "Text"
}

87
sources/Text/TextAPI.uc

@ -667,6 +667,41 @@ public final function array<BaseText> PartsS(string source)
return result; return result;
} }
/**
* Creates and initializes with `source` new `TextTemplate` instance.
*
* @param source Data to initialize new `TextTemplate` with.
* @return `TextTemplate` based on `source`.
* `none` if given `source` is `none`.
*/
public final function TextTemplate MakeTemplate(BaseText source)
{
local TextTemplate newTemplate;
if (source == none) {
return none;
}
newTemplate = TextTemplate(_.memory.Allocate(class'TextTemplate'));
newTemplate.Initialize(source);
return NewTemplate;
}
/**
* Creates and initializes with `source` new `TextTemplate` instance.
*
* @param source Data to initialize new `TextTemplate` with.
* @return `TextTemplate` based on `source`.
* `none` if given `source` is `none`.
*/
public final function TextTemplate MakeTemplate_S(string source)
{
local MutableText wrapper;
local TextTemplate result;
wrapper = FromStringM(source);
result = MakeTemplate(wrapper);
_.memory.Free(wrapper);
return result;
}
/** /**
* Creates a new, empty `MutableText`. * Creates a new, empty `MutableText`.
* *
@ -751,12 +786,56 @@ public final function MutableText FromColoredStringM(string source)
} }
/** /**
* Creates a `Text` that will contain a given formatted `string`. * Creates a `Text` that will contain a parsed formatted string inside
* the given `BaseText`.
*
* To create `MutableText` instead use `FromFormattedM()` method.
*
* @param source Formatted string inside `Text` that will be parsed into
* returned `Text`.
* @return New instance of `Text` that will contain parsed formatted `source`.
*/
public final function Text FromFormatted(BaseText source)
{
local MutableText builder;
local Text result;
if (source == none) {
return none;
}
builder = MutableText(_.memory.Allocate(class'MutableText'));
result = builder.AppendFormatted(source).Copy();
builder.FreeSelf();
return result;
}
/**
* Creates a `MutableText` that will contain a parsed formatted string inside
* the given `BaseText`.
*
* To create `Text` instead use `FromFormatted()` method.
*
* @param source Formatted string inside `Text` that will be parsed into
* returned `Text`.
* @return New instance of `MutableText` that will contain parser formatted
* `source`.
*/
public final function MutableText FromFormattedM(BaseText source)
{
local MutableText newText;
if (source == none) {
return none;
}
newText = MutableText(_.memory.Allocate(class'MutableText'));
return newText.AppendFormatted(source);
}
/**
* Creates a `Text` that will contain parsed formatted `string`.
* *
* To create `MutableText` instead use `FromFormattedStringM()` method. * To create `MutableText` instead use `FromFormattedStringM()` method.
* *
* @param source Formatted `string` that will be copied into returned `Text`. * @param source Formatted `string` that will be copied into returned `Text`.
* @return New instance of `Text` that will contain passed formatted `string`. * @return New instance of `Text` that will contain parsed formatted `string`.
*/ */
public final function Text FromFormattedString(string source) public final function Text FromFormattedString(string source)
{ {
@ -769,13 +848,13 @@ public final function Text FromFormattedString(string source)
} }
/** /**
* Creates a `MutableText` that will contain a given formatted `string`. * Creates a `MutableText` that will contain parsed formatted `string`.
* *
* To create immutable `Text` instead use `FromFormattedString()` method. * To create immutable `Text` instead use `FromFormattedString()` method.
* *
* @param source Formatted `string` that will be copied into * @param source Formatted `string` that will be copied into
* returned `MutableText`. * returned `MutableText`.
* @return New instance of `MutableText` that will contain passed * @return New instance of `MutableText` that will contain parsed
* formatted `string`. * formatted `string`.
*/ */
public final function MutableText FromFormattedStringM(string source) public final function MutableText FromFormattedStringM(string source)

975
sources/Text/TextTemplate.uc

@ -0,0 +1,975 @@
/**
* Class that allows to work with simple text templates in the form of
* "Template example, following can be replaced: %1, %2, %3 or %4" or
* "%%instigator%% {%%rage_color%% raged} %%target_zed%%!".
* Part noted by '%' characters can be replaced with arbitrary values.
* It should be more efficient that simply repeatedly calling
* `MutableText.Replace()` method.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TextTemplate extends AcediaObject;
/**
* # `TextTemplate`
*
* This class allows to do more efficient replacements in the templates of
* form "Template example, following can be replaced: %1, %2, %3 or %4" or
* "%%instigator%% {%%rage_color%% raged} %%target_zed%%!" that simple
* repeating of `MutableText.Replace()` method would allow.
*
* ## Usage
*
* All `TextTemplate`s must be initialized before they can be used.
* `_.text.MakeTemplate()` method is considered a recommended way to create
* a new `TextTemplate` and will initialize it for you. In case you have
* manually allocated `TextTemplate` you need to simply call
* `TextTemplate.Initialize()` method and provide is with a template source
* with designated replaceable part (called *labels*). Any label is designated
* by either:
*
* * Specifying a number after a single '%' character (numeric labels);
* * Or specifying a textual value in between double percent sequence "%%".
*
* Numeric labels can then be replaced with user values by various provided
* `Arg...()` methods and text labels by `TextArg...()` methods. To reuse
* template with another values replacing specified labels, simply call
* `TextTemplate.Reset()` method and fill labels anew.
*
* ### Escaped sequences
*
* `TextTemplate` allows for the escaped sequences in the style of
* formatted strings: the ones based on the '&' character. More precisely, if
* you wish to enter actual '%' character in the template, you can escape it
* with "&%" and it will be treaded as simply '%' character and not a part of
* label definition.
* NOTE: any other escaped sequences will be ignored and translated
* one-to-one. For example, "{%%color%% &{...&}&%!}", if specified that
* "color" -> "$red", will be translated into "{$red &{...&}%!}". This allows
* to use `TextTemplate`s to prepare formatted strings in a more convenient
* fashion.
*
* ### Error handling
*
* `TextTemplate` lack any means of reporting errors in template's formatting
* and tries to correct as many errors as possible.
*
* 1. If after the single '%' character follows not a number, but another
* character - that '%' will be ignored and thrown away;
* 2. After opening text label with double percent "%%" to stop specifying
* the text label it is sufficient to type a single '%' character.
*
* ### Numeric labels values
*
* You can specify any values for numeric labels and they will be filled in
* order. However the proper notation is to start with `1` and then use
* following natural numbers in order (e.g. "%1", "%2", "%3", etc.).
*
* ### Text labels duplicates
*
* It is allowed for the same text label to appear several times in a template
* and each entry will be replaced with a specified user value.
*
* ### Unspecified arguments
*
* If no argument was given for some label - it will simply be replaced with
* empty text.
*/
// *Labels* will be an internal name for the parts of the template that is
// supposed to be replaced. This struct describes one such part.
struct Label
{
// If Label starts with two percent characters ("%%"), then it is
// a text Label and we will remember contents within "%%...%%" in this
// field (`textLabel != none`).
// Otherwise it is a numeric Label and `textLabel` will be set
// to `none`.
var MutableText textLabel;
// For numeric labels - number after that percent character '%'.
// In case numeric labels were specified incorrectly, these values will
// be normalized to start from `1` and increase by +1, while preserving
// their order.
var int numberLabel;
// Before which part (from `parts`) to insert this Label?
var int insertionIndex;
};
var private bool initialized;
// Remembers amount of numeric labels in the template
var private int numericLabelsAmount;
// Arrays of labels and parts in between that template got broken into
// during initialization
var private array<Label> labels;
var private array<MutableText> parts;
// Values user specified as replacements to the labels
var private array<Text> numericArguments;
// Fort text arguments we keep two arrays of equal size: `textLabels` records
// what text Label is replaced and `textArguments` (at the same index)
// records with what it needs to be replaced
var private array<Text> textLabels;
var private array<Text> textArguments;
var private const int TPERCENT, TAMPERSAND;
const CODEPOINT_PERCENT = 37; // '%'
const CODEPOINT_AMPERSAND = 38; // '&'
protected function Finalizer()
{
local int i;
// Clear user input
Reset();
// Clear numeric labels
numericLabelsAmount = 0;
_.memory.FreeMany(parts);
// Clear text Labels
parts.length = 0;
for (i = 0; i < Labels.length; i += 1) {
_.memory.Free(Labels[i].textLabel);
}
labels.length = 0;
// Now we can safely mark caller template as not initialized
initialized = false;
}
/**
* Checks if caller `TextTemplate` was already initialized.
*
* If you use recommended means of creating `TextTemplate`s (`MakeTemplate` and
* `MakeTemplate_S`), then all your templates for not `none` arguments will be
* initialized.
*
* @return `true` if it was initialized and `false` otherwise.
*/
public final function bool IsInitialized()
{
return initialized;
}
/**
* Initializes caller `TextTemplate` with a given `input`.
*
* Initialization should only be performed once for every `TextTemplate` -
* right after its allocation, before any use.
*
* If you use recommended means of creating `TextTemplate`s (`MakeTemplate`),
* then all your templates for not `none` arguments will be initialized.
*
* @param input `string` containing template to prepare `TextTemplate` from.
*/
public final function Initialize_S(string input)
{
local MutableText wrapper;
wrapper = _.text.FromStringM(input);
Initialize(wrapper);
_.memory.Free(wrapper);
}
/**
* Initializes caller `TextTemplate` with a given `input`.
*
* Initialization should only be performed once for every `TextTemplate` -
* right after its allocation, before any use.
*
* If you use recommended means of creating `TextTemplate`s (`MakeTemplate`),
* then all your templates for not `none` arguments will be initialized.
*
* @param input Text containing template to prepare `TextTemplate` from.
*/
public final function Initialize(BaseText input)
{
local Parser parser;
local MutableText nextPart;
local BaseText.Character percent, nextCharacter;
if (input == none) {
return;
}
percent = _.text.CharacterFromCodePoint(CODEPOINT_PERCENT);
parser = input.Parse();
while (!parser.HasFinished())
{
nextPart = MatchUntilUnescapedPercent(parser); // guaranteed success
parts[parts.length] = nextPart;
if (parser.HasFinished()) {
break;
}
// If there is still input - next character is definitely "%"
parser.MCharacter(nextCharacter).Confirm();
parser.MCharacter(nextCharacter); // But what is this?
if (parser.Ok() && _.text.IsCodePoint(nextCharacter, CODEPOINT_PERCENT))
{
// "%%" prefix (already confirmed) - should have text Label
MatchTextLabel(parser, percent);
}
else
{
// single "%" prefix - look for number
MatchNumericLabel(parser.R());
}
}
initialized = true;
_.memory.Free(parser);
NormalizeArguments();
}
private final function MatchTextLabel(
Parser parser,
BaseText.Character percentCharacter)
{
local MutableText nextLabel;
parser.MUntil(nextLabel, percentCharacter).Confirm();
labels[labels.length] = MakeTextLabel(nextLabel);
// Tries to parse "%%", but will be content with a single "%"
parser.Match(T(TPERCENT)).Confirm();
parser.Match(T(TPERCENT)).Confirm();
// Just reset to last properly parsed point in case of the failure
parser.R();
}
private final function MatchNumericLabel(Parser parser)
{
local int nextNumericLabel;
if (parser.MInteger(nextNumericLabel).Ok()) {
labels[labels.length] = MakeNumericLabel(nextNumericLabel);
}
parser.Confirm();
// Just reset to last properly parsed point in case of the failure
parser.R();
}
// Normalize enumeration by replacing them with natural numbers sequence:
// [0, 1, 2, ...] in the same order:
// [2, 6, 3] -> [0, 2, 1]
// [-2, 0, 4, -7] -> [1, 2, 3, 0]
// [1, 1, 2, 1] -> [0, 1, 3, 2]
private final function NormalizeArguments()
{
local int i;
local int nextArgument;
local int lowestArgument, lowestArgumentIndex;
local array<int> argumentsOrder, normalizedArguments;
for (i = 0; i < labels.length; i += 1)
{
if (labels[i].textLabel == none)
{
numericLabelsAmount += 1;
argumentsOrder[argumentsOrder.length] = labels[i].numberLabel;
}
}
normalizedArguments.length = argumentsOrder.length;
while (nextArgument < normalizedArguments.length)
{
// Find next minimal index and record next natural number
// (`nextArgument`) into it
for (i = 0; i < argumentsOrder.length; i += 1)
{
if (argumentsOrder[i] < lowestArgument || i == 0)
{
lowestArgumentIndex = i;
lowestArgument = argumentsOrder[i];
}
}
argumentsOrder[lowestArgumentIndex] = MaxInt;
normalizedArguments[lowestArgumentIndex] = nextArgument;
nextArgument += 1;
}
nextArgument = 0;
for (i = 0; i < labels.length; i += 1)
{
if (labels[i].textLabel == none)
{
labels[i].numberLabel = normalizedArguments[nextArgument];
nextArgument += 1;
}
}
}
private final function Label MakeTextLabel(MutableText Label2)
{
local Label result;
result.textLabel = Label2;
result.insertionIndex = parts.length;
return result;
}
private final function Label MakeNumericLabel(int Label2)
{
local Label result;
result.numberLabel = Label2;
result.insertionIndex = parts.length;
return result;
}
// Skips until next non-escaped (that is "&%") '%' character
// (or until the end).
// It returns any contents inside `parser` before that character replacing
// "&%" with simply "%", but leaving all other escaped sequences intact,
// e.g. "&a" -> "&a" and "&{" -> "&{". This is needed to allow parsing of
// the result of substitution as a formatted string.
private final function MutableText MatchUntilUnescapedPercent(Parser parser)
{
local MutableText result, nextMatched;
local BaseText.Character ampersand, nextCharacter;
local array<Text> delimiters;
ampersand = _.text.CharacterFromCodePoint(CODEPOINT_AMPERSAND);
delimiters[0] = T(TPERCENT);
delimiters[1] = T(TAMPERSAND);
result = _.text.Empty();
while (!parser.HasFinished())
{
parser.MUntilMany(nextMatched, delimiters).Confirm();
result.Append(nextMatched);
_.memory.Free(nextMatched);
if (parser.HasFinished()) {
break;
}
parser.MCharacter(nextCharacter);
// If this is "%": we are done
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_PERCENT))
{
parser.R();
return result;
}
// Otherwise it is "&": skip it and the next character
parser.MCharacter(nextCharacter).Confirm();
if (!parser.Ok())
{
result.AppendCharacter(ampersand);
return result;
}
if (!_.text.IsCodePoint(nextCharacter, CODEPOINT_PERCENT)) {
result.AppendCharacter(ampersand);
}
result.AppendCharacter(nextCharacter);
}
parser.Confirm();
return result;
}
/**
* Resets any data user entered into this template via any "Arg"-methods.
*
* This method allows to reuse a once initialized `TextTemplate` any amount of
* times.
*
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate Reset()
{
if (!initialized) {
return self;
}
_.memory.FreeMany(numericArguments);
_.memory.FreeMany(textLabels);
_.memory.FreeMany(textArguments);
numericArguments.length = 0;
textLabels.length = 0;
textArguments.length = 0;
return self;
}
/**
* Returns amount of numeric arguments inside the caller `TextTemplate`.
*
* @return Amount of numeric arguments inside the caller `TextTemplate`.
* `-1` for uninitialized `TextTemplate`s.
*/
public final function int GetNumericArgsAmount()
{
if (initialized) {
return numericLabelsAmount;
}
return -1;
}
/**
* Returns array of text arguments inside the caller `TextTemplate`.
*
* @return Array of text arguments inside the caller `TextTemplate`.
* Empty array for uninitialized `TextTemplate`s.
*/
public final function array<Text> GetTextArgs()
{
local int i, j;
local bool duplicate;
local array<Text> result;
if (!initialized) {
return result;
}
for (i = 0; i < labels.length; i += 1)
{
if (labels[i].textLabel == none) {
continue;
}
duplicate = false;
for (j = 0; j < result.length; j += 1)
{
if (result[j].Compare(labels[i].textLabel))
{
duplicate = true;
break;
}
}
if (!duplicate && labels[i].textLabel != none) {
result[result.length] = labels[i].textLabel.Copy();
}
}
return result;
}
/**
* Replaces next numeric argument with `argument` value.
*
* If all numeric arguments were already filled this call will do nothing.
*
* @param argument Value to replace next numeric argument in the caller
* `TextTemplate`. `none` values will be ignored (method will do nothing).
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate Arg_S(string argument)
{
local MutableText wrapper;
wrapper = _.text.FromStringM(argument);
Arg(wrapper);
_.memory.Free(wrapper);
return self;
}
/**
* Replaces next numeric argument with `argument` value.
*
* If all numeric arguments were already filled this call will do nothing.
*
* @param argument Value to replace next numeric argument in the caller
* `TextTemplate`. `none` equals to an empty text.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate Arg(BaseText argument)
{
if (numericLabelsAmount <= numericArguments.length) {
return self;
}
if (argument != none) {
numericArguments[numericArguments.length] = argument.Copy();
}
else {
numericArguments[numericArguments.length] = none;
}
return self;
}
/**
* Replaces next numeric argument with `argument` integer value.
*
* If all numeric arguments were already filled this call will do nothing.
*
* @param argument Value to replace next numeric argument in the caller
* `TextTemplate`.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate ArgInt(int argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromIntM(argument);
Arg(textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next numeric argument with `argument` floating point value.
*
* If all numeric arguments were already filled this call will do nothing.
*
* @param argument Value to replace next numeric argument in the caller
* `TextTemplate`.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate ArgFloat(float argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromFloatM(argument);
Arg(textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next numeric argument with `argument` boolean value.
*
* If all numeric arguments were already filled this call will do nothing.
*
* @param argument Value to replace next numeric argument in the caller
* `TextTemplate`.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate ArgBool(bool argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromBoolM(argument);
Arg(textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next numeric argument with `argument` class value.
*
* If all numeric arguments were already filled this call will do nothing.
*
* @param argument Value to replace next numeric argument in the caller
* `TextTemplate`.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate ArgClass(class<Object> argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromClassM(argument);
Arg(textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next text argument with label `label` with `argument` value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArg_S(string label, string argument)
{
local MutableText labelWrapper, argumentWrapper;
labelWrapper = _.text.FromStringM(label);
argumentWrapper = _.text.FromStringM(argument);
TextArg(labelWrapper, argumentWrapper);
_.memory.Free(labelWrapper);
_.memory.Free(argumentWrapper);
return self;
}
/**
* Replaces next text argument with label `label` with `argument` value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with. `none` is equal to an empty text.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArg(BaseText label, BaseText argument)
{
local int i;
if (label == none) {
return self;
}
for (i = 0; i < textLabels.length; i += 1)
{
if (label.Compare(textLabels[i]))
{
_.memory.Free(textArguments[i]);
if (argument != none) {
textArguments[i] = argument.Copy();
}
else {
textArguments[i] = none;
}
return self;
}
}
if (label != none) {
textLabels[textLabels.length] = label.Copy();
}
else {
textLabels[textLabels.length] = none;
}
if (argument != none) {
textArguments[i] = argument.Copy();
}
else {
textArguments[i] = none;
}
return self;
}
/**
* Replaces next text argument with label `label` with integer `argument`
* value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgInt_S(string label, int argument)
{
local MutableText labelWrapper;
local MutableText textRepresentation;
labelWrapper = _.text.FromStringM(label);
textRepresentation = _.text.FromIntM(argument);
TextArg(labelWrapper, textRepresentation);
_.memory.Free(textRepresentation);
_.memory.Free(labelWrapper);
return self;
}
/**
* Replaces next text argument with label `label` with integer `argument`
* value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgInt(BaseText label, int argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromIntM(argument);
TextArg(label, textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next text argument with label `label` with floating point
* `argument` value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgFloat_S(string label, float argument)
{
local MutableText labelWrapper;
local MutableText textRepresentation;
labelWrapper = _.text.FromStringM(label);
textRepresentation = _.text.FromFloatM(argument);
TextArg(labelWrapper, textRepresentation);
_.memory.Free(textRepresentation);
_.memory.Free(labelWrapper);
return self;
}
/**
* Replaces next text argument with label `label` with floating point
* `argument` value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgFloat(BaseText label, float argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromFloatM(argument);
TextArg(label, textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next text argument with label `label` with boolean `argument`
* value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgBool_S(string label, bool argument)
{
local MutableText labelWrapper;
local MutableText textRepresentation;
labelWrapper = _.text.FromStringM(label);
textRepresentation = _.text.FromBoolM(argument);
TextArg(labelWrapper, textRepresentation);
_.memory.Free(textRepresentation);
_.memory.Free(labelWrapper);
return self;
}
/**
* Replaces next text argument with label `label` with boolean `argument`
* value.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgBool(BaseText label, bool argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromBoolM(argument);
TextArg(label, textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
/**
* Replaces next text argument with label `label` with class value `argument`.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgClass_S(
string label,
class<Object> argument)
{
local MutableText labelWrapper;
local MutableText textRepresentation;
labelWrapper = _.text.FromStringM(label);
textRepresentation = _.text.FromClassM(argument);
TextArg(labelWrapper, textRepresentation);
_.memory.Free(textRepresentation);
_.memory.Free(labelWrapper);
return self;
}
/**
* Replaces next text argument with label `label` with class value `argument`.
*
* If all text argument with that label was already set, then it will override
* previous value.
*
* @param label Label of the text argument to replace.
* @param argument Value to replace specified text argument in the caller
* `TextTemplate` with.
* @return Reference to the caller `TextTemplate` to allow for method chaining.
*/
public final function TextTemplate TextArgClass(
BaseText label,
class<Object> argument)
{
local MutableText textRepresentation;
textRepresentation = _.text.FromClassM(argument);
TextArg(label, textRepresentation);
_.memory.Free(textRepresentation);
return self;
}
private final function Text BorrowNumericArg(int index)
{
if (index < 0) return none;
if (index >= numericArguments.length) return none;
return numericArguments[index];
}
private final function Text BorrowTextArg(BaseText Label)
{
local int i;
if (Label == none) {
return none;
}
for (i = 0; i < textLabels.length; i += 1)
{
if (Label.Compare(textLabels[i])) {
return textArguments[i];
}
}
return none;
}
/**
* Assembles initialized `TextTemplate` from provided arguments into a final
* `Text` result.
*
* Unspecified arguments will be replaced with empty texts.
*
* Returns `string`. To return mutable `Text` or `MutableText` use
* `Collect()` or `CollectM()`.
*
* @return Result of replacing all argument inside caller `TextTemplate` with
* arguments, specified by user.
*/
public final function string Collect_S()
{
return _.text.ToString(CollectM());
}
/**
* Assembles initialized `TextTemplate` from provided arguments into a final
* `Text` result.
*
* Unspecified arguments will be replaced with empty texts.
*
* Returns immutable `Text`. To return mutable `MutableText` use `CollectM()`.
*
* @return Result of replacing all argument inside caller `TextTemplate` with
* arguments, specified by user.
*/
public final function Text Collect()
{
local MutableText mutableResult;
mutableResult = CollectM();
if (mutableResult != none) {
return mutableResult.IntoText();
}
return none;
}
/**
* Assembles initialized `TextTemplate` from provided arguments into a final
* `Text` result.
*
* Unspecified arguments will be replaced with empty texts.
*
* Returns mutable `MutableText`. To return immutable `Text` use `CollectM()`.
*
* @return Result of replacing all argument inside caller `TextTemplate` with
* arguments, specified by user.
*/
public final function MutableText CollectM()
{
local int i, labelCounter;
local Label nextLabel;
local MutableText builder;
if (!initialized) {
return none;
}
builder = _.text.Empty();
for (i = 0; i < parts.length; i += 1)
{
if ( labelCounter < labels.length
&& labels[labelCounter].insertionIndex == i)
{
nextLabel = labels[labelCounter];
if (nextLabel.textLabel == none) {
builder.Append(BorrowNumericArg(nextLabel.numberLabel));
}
else {
builder.Append(BorrowTextArg(nextLabel.textLabel));
}
labelCounter += 1;
}
builder.Append(parts[i]);
}
while (labelCounter < labels.length)
{
nextLabel = labels[labelCounter];
if (nextLabel.textLabel == none) {
builder.Append(BorrowNumericArg(nextLabel.numberLabel));
}
else {
builder.Append(BorrowTextArg(nextLabel.textLabel));
}
labelCounter += 1;
}
return builder;
}
/**
* Assembles initialized `TextTemplate` from provided arguments and parses
* resulting text as formatted string.
*
* Allows to do things like specify color inside formatted strings:
* "{%%color_arg%% ColoredText}".
*
* Unspecified arguments will be replaced with empty texts.
*
* Returns immutable `Text`. To return mutable `MutableText` use `CollectM()`.
*
* @return Result of replacing all argument inside caller `TextTemplate` with
* arguments, specified by user and then parsing that intermediate result
* as a formatting string.
*/
public final function Text CollectFormatted()
{
local Text result;
local MutableText source;
source = CollectM();
if (source == none) {
return none;
}
result = _.text.FromFormatted(source);
_.memory.Free(source);
return result;
}
/**
* Assembles initialized `TextTemplate` from provided arguments and parses
* resulting text as formatted string.
*
* Allows to do things like specify color inside formatted strings:
* "{%%color_arg%% ColoredText}".
*
* Unspecified arguments will be replaced with empty texts.
*
* Returns mutable `MutableText`. To return immutable `Text` use `CollectM()`.
*
* @return Result of replacing all argument inside caller `TextTemplate` with
* arguments, specified by user and then parsing that intermediate result
* as a formatting string.
*/
public final function MutableText CollectFormattedM()
{
local MutableText result, source;
source = CollectM();
if (source == none) {
return none;
}
result = _.text.FromFormattedM(source);
_.memory.Free(source);
return result;
}
defaultproperties
{
TPERCENT = 0
stringConstants(0) = "%"
TAMPERSAND = 1
stringConstants(1) = "&&"
}
Loading…
Cancel
Save