diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index 6316528..8d0dbc8 100644 --- a/sources/Text/MutableText.uc +++ b/sources/Text/MutableText.uc @@ -19,41 +19,59 @@ */ class MutableText extends Text; -var private int CODEPOINT_NEWLINE; +var private int CODEPOINT_NEWLINE, CODEPOINT_ACCENT; -// Every formatted `string` essentially consists of multiple differently -// formatted (colored) parts. Such `string`s will be more convenient for us to -// work with if we separate them from each other. -// This structure represents one such block: maximum uninterrupted -// substring, every character of which has identical formatting. -// Do note that a single block does not define text formatting, - -// it is defined by the whole sequence of blocks before it -// (if `isOpening == false` you only know that you should change previous -// formatting, but you do not know to what). -struct FormattedBlock +enum FormattedStackCommandType +{ + // Push more data onto formatted stack + FST_StackPush, + // Pop data from formatted stack + FST_StackPop, + // Swap the top value on the formatting stack + FST_StackSwap +}; + +// Formatted `string` is separated into several (possibly nested) parts, +// each with its own formatting. These can be easily handled with a formatting +// stack: +// * Each time a new section opens ("{ contents; - // Formatting tag for this block - // (ignored for `isOpening == false`) - var string tag; - // Whitespace symbol that separates tag from the `contents`; - // For the purposes of reassembling a `string` broken into blocks. - // (ignored for `isOpening == false`) - var Character delimiter; + var array contents; + // Formatting tag for the next block + // (only used for `FST_StackPush` command type) + var MutableText tag; + // Formatting character for the "^"-type tag + // (only used for `FST_StackSwap` command type) + var Character charTag; }; -// Appending formatted `string` into the `MutableText` first requires to -// split it into series of `FormattedBlock` and then extract code points with -// the proper formatting from it. +// Appending formatted `string` into the `MutableText` first requires its +// transformation into series of `FormattedStackCommand` and then their +// execution to assemble the `MutableText`. +// First element of `stackCommands` is special and is used solely as +// a container for unformatted data. It should not be used to execute +// formatting stack commands. // This variable contains intermediary data. -var array splitBlocks; +var array stackCommands; // Formatted `string` can have an arbitrary level of folded format definitions, // this array is used as a stack to keep track of opened formatting blocks // when appending formatted `string`. -var array formattingStack; +var array formattingStack; /** * Clears all current data from the caller `MutableText` instance. @@ -227,6 +245,28 @@ public final function MutableText AppendColoredString( return self; } +/** + * Appends contents of the formatted `Text` to the caller `MutableText`. + * + * @param source `Text` (with formatted string contents) to be + * appended to the caller `MutableText`. + * @param defaultFormatting Formatting to apply to `source`'s character that + * do not have it specified. For example, `defaultFormatting.isColored`, + * but some of `other`'s characters do not have a color defined - + * they will be appended with a specified color. + * @return Caller `MutableText` to allow for method chaining. + */ +public final function MutableText AppendFormatted( + Text source, + optional Formatting defaultFormatting) +{ + local Parser parser; + parser = _.text.Parse(source); + AppendFormattedParser(parser, defaultFormatting); + parser.FreeSelf(); + return self; +} + /** * Appends contents of the formatted `string` to the caller `MutableText`. * @@ -239,69 +279,99 @@ public final function MutableText AppendColoredString( public final function MutableText AppendFormattedString( string source, optional Formatting defaultFormatting) +{ + local Parser parser; + parser = _.text.ParseString(source); + AppendFormattedParser(parser, defaultFormatting); + parser.FreeSelf(); + return self; +} + +/** + * Appends contents of the formatted `string` to the caller `MutableText`. + * + * @param source Formatted `string` to be appended to + * the caller `MutableText`. + * @param defaultFormatting Formatting to be used for `source`'s characters + * that have no color information defined. + * @return Caller `MutableText` to allow for method chaining. + */ +private final function MutableText AppendFormattedParser( + Parser sourceParser, + optional Formatting defaultFormatting) { local int i; - local Parser parser; - SplitFormattedStringIntoBlocks(source); - if (splitBlocks.length <= 0) { + local Parser tagParser; + BuildFormattingStackCommands(sourceParser); + if (stackCommands.length <= 0) { return self; } SetupFormattingStack(defaultFormatting); - parser = Parser(_.memory.Allocate(class'Parser')); - // First element of `decomposedSource` is special and has - // no color information, - // see `SplitFormattedStringIntoBlocks()` for details. + tagParser = Parser(_.memory.Allocate(class'Parser')); SetFormatting(defaultFormatting); - AppendManyCodePoints(splitBlocks[0].contents); - for (i = 1; i < splitBlocks.length; i += 1) + // First element of color stack is special and has no color information; + // see `BuildFormattingStackCommands()` for details. + AppendManyCodePoints(stackCommands[0].contents); + for (i = 1; i < stackCommands.length; i += 1) { - if (splitBlocks[i].isOpening) + if (stackCommands[i].type == FST_StackPush) { - parser.InitializeS(splitBlocks[i].tag); - SetFormatting(PushIntoFormattingStack(parser)); + tagParser.Initialize(stackCommands[i].tag); + SetFormatting(PushIntoFormattingStack(tagParser)); } - else { + else if (stackCommands[i].type == FST_StackPop) { SetFormatting(PopFormattingStack()); } - AppendManyCodePoints(splitBlocks[i].contents); + else if (stackCommands[i].type == FST_StackSwap) { + SetFormatting(SwapFormattingStack(stackCommands[i].charTag)); + } + AppendManyCodePoints(stackCommands[i].contents); + _.memory.Free(stackCommands[i].tag); } - _.memory.Free(parser); + stackCommands.length = 0; + _.memory.Free(tagParser); return self; } -// Function that breaks formatted string into array of `FormattedBlock`s. -// Returned array is guaranteed to always have at least one block. -// First block in array always corresponds to part of the input string +// Function that parses formatted `string` into array of +// `FormattedStackCommand`s. +// Returned array is guaranteed to always have at least one element. +// First element in array always corresponds to part of the input string // (`source`) without any formatting defined, even if it's empty. -// This is to avoid `FormattedBlock` having a third option besides two defined -// by `isOpening` variable. -private final function SplitFormattedStringIntoBlocks(string source) +// This is to avoid having fourth command type, only usable at the beginning. +private final function BuildFormattingStackCommands(Parser parser) { - local Parser parser; - local Character nextCharacter; - local FormattedBlock nextBlock; - splitBlocks.length = 0; - parser = _.text.ParseString(source); + local Character nextCharacter; + local FormattedStackCommand nextCommand; + stackCommands.length = 0; while (!parser.HasFinished()) { parser.MCharacter(nextCharacter); - // New formatted block by "{" + // New command by "{" if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) { - splitBlocks[splitBlocks.length] = nextBlock; - nextBlock = CreateFormattedBlock(true); - parser.MUntilS(nextBlock.tag,, true) - .MCharacter(nextBlock.delimiter); - if (!parser.Ok()) { - break; - } + stackCommands[stackCommands.length] = nextCommand; + nextCommand = CreateStackCommand(FST_StackPush); + parser.MUntil(nextCommand.tag,, true) + .MCharacter(nextCommand.charTag); // Simply to skip a char continue; } - // New formatted block by "}" + // New command by "}" if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) { - splitBlocks[splitBlocks.length] = nextBlock; - nextBlock = CreateFormattedBlock(false); + stackCommands[stackCommands.length] = nextCommand; + nextCommand = CreateStackCommand(FST_StackPop); + continue; + } + // New command by "^" + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) + { + stackCommands[stackCommands.length] = nextCommand; + nextCommand = CreateStackCommand(FST_StackSwap); + parser.MCharacter(nextCommand.charTag); + if (!parser.Ok()) { + break; + } continue; } // Escaped sequence @@ -311,25 +381,23 @@ private final function SplitFormattedStringIntoBlocks(string source) if (!parser.Ok()) { break; } - nextBlock.contents[nextBlock.contents.length] = nextCharacter.codePoint; + nextCommand.contents[nextCommand.contents.length] = + nextCharacter.codePoint; } - // Only put in empty block if there is nothing else. - if (nextBlock.contents.length > 0 || splitBlocks.length == 0) { - splitBlocks[splitBlocks.length] = nextBlock; + // Only put in empty command if there is nothing else. + if (nextCommand.contents.length > 0 || stackCommands.length == 0) { + stackCommands[stackCommands.length] = nextCommand; } - _.memory.Free(parser); } -// Following two functions are to maintain a "color stack" that will -// remember unclosed colors (new colors are obtained from a parser) defined in -// formatted string, on order. +// Following four functions are to maintain a "color stack" that will +// remember unclosed colors (new colors are obtained from formatting commands +// sequence) defined in formatted string, in order. // Stack array always contains one element, defined by // the `SetupFormattingStack()` call. It corresponds to the default formatting // that will be used when we pop all the other elements. // It is necessary to deal with possible folded formatting definitions in // formatted strings. -// For storing the color information we simply use `Text.Character`, -// ignoring all information that is not related to colors. private final function SetupFormattingStack(Text.Formatting defaultFormatting) { formattingStack.length = 0; @@ -347,6 +415,20 @@ private final function Formatting PushIntoFormattingStack( return newFormatting; } +private final function Formatting SwapFormattingStack(Character tagCharacter) +{ + local Formatting updatedFormatting; + if (formattingStack.length <= 0) { + return updatedFormatting; + } + updatedFormatting = formattingStack[formattingStack.length - 1]; + if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.color)) { + updatedFormatting.isColored = true; + } + formattingStack[formattingStack.length - 1] = updatedFormatting; + return updatedFormatting; +} + private final function Formatting PopFormattingStack() { local Formatting result; @@ -357,12 +439,13 @@ private final function Formatting PopFormattingStack() return result; } -// Helper method for a quick creation of a new `FormattedBlock` -private final function FormattedBlock CreateFormattedBlock(bool isOpening) +// Helper method for a quick creation of a new `FormattedStackCommand` +private final function FormattedStackCommand CreateStackCommand( + FormattedStackCommandType stackCommandType) { - local FormattedBlock newBlock; - newBlock.isOpening = isOpening; - return newBlock; + local FormattedStackCommand newCommand; + newCommand.type = stackCommandType; + return newCommand; } /** @@ -477,5 +560,6 @@ public final function MutableText ChangeFormatting( defaultproperties { - CODEPOINT_NEWLINE = 10 + CODEPOINT_NEWLINE = 10 + CODEPOINT_ACCENT = 94 } \ No newline at end of file diff --git a/sources/Text/Tests/TEST_Text.uc b/sources/Text/Tests/TEST_Text.uc index ffaf18d..6a64e8f 100644 --- a/sources/Text/Tests/TEST_Text.uc +++ b/sources/Text/Tests/TEST_Text.uc @@ -44,10 +44,16 @@ protected static function TESTS() } protected static function Test_TextCreation() +{ + Context("Testing basic functionality for creating `Text` objects."); + SubTest_TextCreationSimply(); + SubTest_TextCreationFormattedComplex(); +} + +protected static function SubTest_TextCreationSimply() { local string plainString, coloredString, formattedString; local Text plain, colored, formatted; - Context("Testing basic functionality for creating `Text` objects."); Issue("`Text` object is not properly created from the plain string."); plainString = "Prepare to DIE and be reborn!"; plain = class'Text'.static.ConstFromPlainString(plainString); @@ -62,10 +68,27 @@ protected static function Test_TextCreation() TEST_ExpectTrue(colored.ToColoredString() == coloredString); Issue("`Text` object is not properly created from the formatted string."); - formattedString = "Prepare to {rgb(255,0,0) DIE} and be reborn!"; + formattedString = "Prepare to {rgb(255,0,0) DIE ^yand} be ^7reborn!"; formatted = class'Text'.static.ConstFromFormattedString(formattedString); TEST_ExpectNotNone(formatted); - TEST_ExpectTrue(formatted.ToFormattedString() == formattedString); + TEST_ExpectTrue( formatted.ToFormattedString() + == ("Prepare to {rgb(255,0,0) DIE }{rgb(255,255,0) and} be" + @ "{rgb(255,255,255) reborn!}")); +} + +protected static function SubTest_TextCreationFormattedComplex() +{ + local string input, output; + local Text formatted; + Issue("`Text` object is not properly created from the formatted string."); + input = "This {$green i^4^5^cs q{#0f0f0f uit}^1e} a {rgb(45,234,154)" + @ "{$white ^bcomplex {$white ex}amp^ple}!}"; + output = "This {rgb(0,128,0) i}{rgb(0,255,255) s q}{rgb(15,15,15) uit}" + $ "{rgb(255,0,0) e} a {rgb(0,0,255) complex }{rgb(255,255,255) ex}" + $ "{rgb(0,0,255) amp}{rgb(255,0,255) le}{rgb(45,234,154) !}"; + formatted = class'Text'.static.ConstFromFormattedString(input); + TEST_ExpectNotNone(formatted); + TEST_ExpectTrue(formatted.ToFormattedString() == output); } protected static function Test_TextCopy()