From 47a4095bb191eb807d4ee5110bf2c571e8f7972e Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 27 Jun 2022 04:01:41 +0700 Subject: [PATCH] Add `TextTemplate` class --- sources/Manifest.uc | 29 +- sources/Text/Tests/TEST_TextTemplate.uc | 361 +++++++++ sources/Text/TextAPI.uc | 87 ++- sources/Text/TextTemplate.uc | 975 ++++++++++++++++++++++++ 4 files changed, 1434 insertions(+), 18 deletions(-) create mode 100644 sources/Text/Tests/TEST_TextTemplate.uc create mode 100644 sources/Text/TextTemplate.uc diff --git a/sources/Manifest.uc b/sources/Manifest.uc index e1fead3..4f5e9b7 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -42,18 +42,19 @@ defaultproperties testCases(11) = class'TEST_JSON' testCases(12) = class'TEST_TextCache' testCases(13) = class'TEST_FormattedStrings' - testCases(14) = class'TEST_User' - testCases(15) = class'TEST_Memory' - testCases(16) = class'TEST_DynamicArray' - testCases(17) = class'TEST_AssociativeArray' - testCases(18) = class'TEST_CollectionsMixed' - testCases(19) = class'TEST_Iterator' - testCases(20) = class'TEST_Command' - testCases(21) = class'TEST_CommandDataBuilder' - testCases(22) = class'TEST_LogMessage' - testCases(23) = class'TEST_DatabaseCommon' - testCases(24) = class'TEST_LocalDatabase' - testCases(25) = class'TEST_AcediaConfig' - testCases(26) = class'TEST_UTF8EncoderDecoder' - testCases(27) = class'TEST_AvariceStreamReader' + testCases(14) = class'TEST_TextTemplate' + testCases(15) = class'TEST_User' + testCases(16) = class'TEST_Memory' + testCases(17) = class'TEST_DynamicArray' + testCases(18) = class'TEST_AssociativeArray' + testCases(19) = class'TEST_CollectionsMixed' + testCases(20) = class'TEST_Iterator' + testCases(21) = class'TEST_Command' + testCases(22) = class'TEST_CommandDataBuilder' + testCases(23) = class'TEST_LogMessage' + testCases(24) = class'TEST_DatabaseCommon' + testCases(25) = class'TEST_LocalDatabase' + testCases(26) = class'TEST_AcediaConfig' + testCases(27) = class'TEST_UTF8EncoderDecoder' + testCases(28) = class'TEST_AvariceStreamReader' } \ No newline at end of file diff --git a/sources/Text/Tests/TEST_TextTemplate.uc b/sources/Text/Tests/TEST_TextTemplate.uc new file mode 100644 index 0000000..cd49fd7 --- /dev/null +++ b/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 . + */ +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("")); + 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")) + .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("")).Arg(P("Now this is too much!")); + 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")) + .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("")).Arg(P("heh")); + TEST_ExpectTrue(instance.Collect().ToString() + == "This is argument: heh! And this: !!"); + 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("")).Arg(P("heh")); + TEST_ExpectTrue(instance.Collect().ToString() + == "This is argument: heh! And this: !!"); + 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 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("")); + 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("")); + TEST_ExpectTrue(instance.Collect().ToString() + == "This is argument: 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 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" +} \ No newline at end of file diff --git a/sources/Text/TextAPI.uc b/sources/Text/TextAPI.uc index 9c87c53..ae2757d 100644 --- a/sources/Text/TextAPI.uc +++ b/sources/Text/TextAPI.uc @@ -667,6 +667,41 @@ public final function array PartsS(string source) 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`. * @@ -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. * * @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) { @@ -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. * * @param source Formatted `string` that will be copied into * returned `MutableText`. - * @return New instance of `MutableText` that will contain passed + * @return New instance of `MutableText` that will contain parsed * formatted `string`. */ public final function MutableText FromFormattedStringM(string source) diff --git a/sources/Text/TextTemplate.uc b/sources/Text/TextTemplate.uc new file mode 100644 index 0000000..c1aa044 --- /dev/null +++ b/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 . + */ +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