From acc31767e5a922e71c25a45764c46312c92c9487 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 17 Jun 2022 04:02:51 +0700 Subject: [PATCH] Refactored new code for parsing formatted strings --- .../FormattedStrings/FormattedStringData.uc | 404 ------------ .../FormattedStrings/FormattingCommandList.uc | 296 --------- .../FormattingCommandsSequence.uc | 316 ++++++++++ ...ingErrors.uc => FormattingErrorsReport.uc} | 96 +-- .../FormattingStringParser.uc | 548 ++++++++++++++++ sources/Text/MutableText.uc | 20 +- sources/Text/Tests/TEST_FormattedStrings.uc | 586 +++++++++--------- 7 files changed, 1210 insertions(+), 1056 deletions(-) delete mode 100644 sources/Text/FormattedStrings/FormattedStringData.uc delete mode 100644 sources/Text/FormattedStrings/FormattingCommandList.uc create mode 100644 sources/Text/FormattedStrings/FormattingCommandsSequence.uc rename sources/Text/FormattedStrings/{FormattingErrors.uc => FormattingErrorsReport.uc} (68%) create mode 100644 sources/Text/FormattedStrings/FormattingStringParser.uc diff --git a/sources/Text/FormattedStrings/FormattedStringData.uc b/sources/Text/FormattedStrings/FormattedStringData.uc deleted file mode 100644 index c00470c..0000000 --- a/sources/Text/FormattedStrings/FormattedStringData.uc +++ /dev/null @@ -1,404 +0,0 @@ -/** - * Object that is created from *formatted string* (or `Text`) and stores - * information about formatting used in said string. Was introduced instead of - * a simple method in `MutableText` to: - * 1. Allow for reporting errors caused by badly specified colors; - * 2. Allow for a more complicated case of specifying a color gradient - * range. - * 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 FormattedStringData extends AcediaObject - dependson(Text) - dependson(FormattingErrors) - dependson(FormattingCommandList); - -struct FormattingInfo -{ - var bool colored; - var Color plainColor; - var bool gradient; - var array gradientColors; - var array gradientPoints; - var int gradientStart; - var float gradientLength; -}; -// 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; -// Keep top element copied into a separate variable for quicker access. -// Must maintain invariant: if `formattingStack.length > 0` -// then `formattingStack[formattingStack.length - 1] == formattingStackHead`. -var FormattingInfo formattingStackHead; - -var private FormattingCommandList commands; -var private MutableText result; -var private FormattingErrors errors; - -protected function Finalizer() -{ - formattingStack.length = 0; - _.memory.Free(commands); - _.memory.Free(errors); - _.memory.Free(result); - commands = none; - errors = none; - result = none; -} - -public static final function FormattedStringData FromText( - Text source, - optional bool doReportErrors) -{ - local FormattedStringData newData; - if (source == none) { - return none; - } - newData = - FormattedStringData(__().memory.Allocate(class'FormattedStringData')); - if (doReportErrors) - { - newData.errors = - FormattingErrors(__().memory.Allocate(class'FormattingErrors')); - } - newData.commands = class'FormattingCommandList'.static - .FromText(source, newData.errors); - newData.result = __().text.Empty(); - newData.BuildSelf(); - __().memory.Free(newData.commands); - newData.commands = none; - return newData; -} - -public final function Text GetResult() -{ - return result.Copy(); -} - -public final function MutableText GetResultM() -{ - return result.MutableCopy(); -} - -public final function FormattingErrors BorrowErrors() -{ - return errors; -} - -private final function BuildSelf() -{ - local int i, j, nextCharacterIndex; - local Text.Formatting defaultFormatting; - local array nextContents; - local FormattingCommandList.FormattingCommand nextCommand; - SetupFormattingStack(defaultFormatting); - // First element of color stack is special and has no color information; - // see `BuildFormattingStackCommands()` for details. - nextCommand = commands.GetCommand(0); - nextContents = nextCommand.contents; - result.AppendManyRawCharacters(nextContents); - nextCharacterIndex = nextContents.length; - _.memory.Free(nextCommand.tag); - for (i = 1; i < commands.GetAmount(); i += 1) - { - nextCommand = commands.GetCommand(i); - if (nextCommand.type == FST_StackPush) { - PushIntoFormattingStack(nextCommand); - } - else if (nextCommand.type == FST_StackPop) { - PopFormattingStack(); - } - else if (nextCommand.type == FST_StackSwap) { - SwapFormattingStack(nextCommand.charTag); - } - nextContents = nextCommand.contents; - if (IsCurrentFormattingGradient()) - { - for (j = 0; j < nextContents.length; j += 1) - { - result.AppendRawCharacter(nextContents[j], GetFormattingFor(nextCharacterIndex)); - nextCharacterIndex += 1; - } - } - else - { - result.AppendManyRawCharacters(nextContents, GetFormattingFor(nextCharacterIndex)); - nextCharacterIndex += nextContents.length; - } - _.memory.Free(nextCommand.tag); - } -} - -// 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. -private final function SetupFormattingStack(Text.Formatting defaultFormatting) -{ - local FormattingInfo defaultFormattingInfo; - defaultFormattingInfo.colored = defaultFormatting.isColored; - defaultFormattingInfo.plainColor = defaultFormatting.color; - if (formattingStack.length > 0) { - formattingStack.length = 0; - } - formattingStack[0] = defaultFormattingInfo; - formattingStackHead = defaultFormattingInfo; -} - -private final function bool IsCurrentFormattingGradient() -{ - if (formattingStack.length <= 0) { - return false; - } - return formattingStackHead.gradient; -} - -private final function Text.Formatting GetFormattingFor(int index) -{ - local Text.Formatting emptyFormatting; - if (formattingStack.length <= 0) return emptyFormatting; - if (!formattingStackHead.colored) return emptyFormatting; - - return _.text.FormattingFromColor(GetColorFor(index)); -} -//FormattedStringData Package.FormattedStringData (Function AcediaCore.FormattedStringData.GetColorFor:00FC) Accessed array 'gradientColors' out of bounds (2/2) -private final function Color GetColorFor(int index) -{ - local int i; - local float indexPosition, leftPosition, rightPosition; - local array points; - local Color leftColor, rightColor, resultColor; - if (formattingStack.length <= 0) { - return resultColor; - } - if (!formattingStackHead.gradient) { - return formattingStackHead.plainColor; - } - indexPosition = float(index - formattingStackHead.gradientStart) / - formattingStackHead.gradientLength; - points = formattingStackHead.gradientPoints; - for (i = 1; i < points.length; i += 1) - { - if (points[i - 1] <= indexPosition && indexPosition <= points[i]) - { - leftPosition = points[i - 1]; - rightPosition = points[i]; - leftColor = formattingStackHead.gradientColors[i - 1]; - rightColor = formattingStackHead.gradientColors[i]; - break; - } - } - indexPosition = - (indexPosition - leftPosition) / (rightPosition - leftPosition); - resultColor.R = Lerp(indexPosition, leftColor.R, rightColor.R); - resultColor.G = Lerp(indexPosition, leftColor.G, rightColor.G); - resultColor.B = Lerp(indexPosition, leftColor.B, rightColor.B); - resultColor.A = Lerp(indexPosition, leftColor.A, rightColor.A); - return resultColor; -} - -private final function PushIntoFormattingStack( - FormattingCommandList.FormattingCommand formattingCommand) -{ - formattingStackHead = ParseFormattingInfo(formattingCommand.tag); - formattingStackHead.gradientStart = formattingCommand.openIndex; - formattingStackHead.gradientLength = - float(formattingCommand.closeIndex - formattingCommand.openIndex); - formattingStack[formattingStack.length] = formattingStackHead; -} - -private final function SwapFormattingStack(Text.Character tagCharacter) -{ - local FormattingInfo updatedFormatting; - if (formattingStack.length > 0) { - updatedFormatting = formattingStackHead; - } - if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.plainColor)) - { - updatedFormatting.colored = true; - updatedFormatting.gradient = false; - } - else { - Report(FSE_BadShortColorTag, _.text.FromString("^" $ Chr(tagCharacter.codePoint))); - } - formattingStackHead = updatedFormatting; - if (formattingStack.length > 0) { - formattingStack[formattingStack.length - 1] = updatedFormatting; - } - else { - formattingStack[0] = updatedFormatting; - } -} - -private final function PopFormattingStack() -{ - // Remove the top of the stack - if (formattingStack.length > 0) { - formattingStack.length = formattingStack.length - 1; - } - // Update the stack head copy - if (formattingStack.length > 0) { - formattingStackHead = formattingStack[formattingStack.length - 1]; - } -} - -private final function FormattingInfo ParseFormattingInfo(Text colorTag) -{ - local int i; - local Parser colorParser; - local Color nextColor; - local array specifiedColors; - local Text.Character tildeCharacter; - local array gradientColors; - local array gradientPoints; - local FormattingInfo resultInfo; - if (colorTag.IsEmpty()) - { - Report(FSE_EmptyColorTag); - return resultInfo; // not colored - } - tildeCharacter = _.text.GetCharacter("~"); - specifiedColors = colorTag.SplitByCharacter(tildeCharacter, true); - for (i = 0; i < specifiedColors.length; i += 1) - { - colorParser = _.text.Parse(specifiedColors[i]); - if (_.color.ParseWith(colorParser, nextColor)) - { - colorParser.Confirm(); - gradientColors[gradientColors.length] = nextColor; - gradientPoints[gradientPoints.length] = ParsePoint(colorParser); - } - else { - Report(FSE_BadColor, specifiedColors[i]); - } - _.memory.Free(colorParser); - } - _.memory.FreeMany(specifiedColors); - gradientPoints = NormalizePoints(gradientPoints); - resultInfo.colored = (gradientColors.length > 0); - resultInfo.gradient = (gradientColors.length > 1); - resultInfo.gradientColors = gradientColors; - resultInfo.gradientPoints = gradientPoints; - if (gradientColors.length > 0) { - resultInfo.plainColor = gradientColors[0]; - } - return resultInfo; -} - -private final function float ParsePoint(Parser parser) -{ - local float point; - local Parser.ParserState initialState; - if (!parser.Ok() || parser.HasFinished()) { - return -1; - } - initialState = parser.GetCurrentState(); - // [Necessary part] Should starts with "[" - if (!parser.Match(P("[")).Ok()) - { - Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); - return -1; - } - // [Necessary part] Try parsing number - parser.MNumber(point).Confirm(); - if (!parser.Ok()) - { - Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); - return -1; - } - // [Optional part] Check if number is a percentage - if (parser.Match(P("%")).Ok()) { - point *= 0.01; - } - // This either confirms state of parsing "%" (on success) - // or reverts to the previous state, just after parsing the number - // (on failure) - parser.Confirm(); - parser.R(); - // [Necessary part] Have to have closing parenthesis - if (!parser.HasFinished()) { - parser.Match(P("]")).Confirm(); - } - // Still return `point`, even if there was no closing parenthesis, - // since that is likely what user wants - if (!parser.Ok()) { - Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); - } - return point; -} -/*FIRST-POPOPOINTS 0.00 -1.00 -1.00 -1.00 1.00 5 -PRE-POPOPOINTS 0.00 -1.00 0.00 -1.00 1.00 5 */ -private final function array NormalizePoints(array points) -{ - local int i, j; - local int negativeSegmentStart, negativeSegmentLength; - local float lowerBound, upperBound; - local bool foundNegative; - if (points.length > 1) - { - points[0] = 0.0; - points[points.length - 1] = 1.0; - } - for (i = 1; i < points.length - 1; i += 1) - { - if (points[i] <= 0 || points[i] > 1 || points[i] <= points[i - 1]) { - points[i] = -1; - } - } - for (i = 1; i < points.length; i += 1) - { - if (foundNegative && points[i] > 0) - { - upperBound = points[i]; - for (j = negativeSegmentStart; j < i; j += 1) - { - points[j] = Lerp( float(j - negativeSegmentStart + 1) / float(negativeSegmentLength + 1), - lowerBound, upperBound); - } - negativeSegmentLength = 0; - } - if (!foundNegative && points[i] < 0) - { - lowerBound = points[i - 1]; - negativeSegmentStart = i; - } - foundNegative = (points[i] < 0); - if (foundNegative) { - negativeSegmentLength += 1; - } - } - return points; -} - -public final function Report( - FormattingErrors.FormattedDataErrorType type, - optional Text cause) -{ - if (errors == none) { - return; - } - errors.Report(type, cause); -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Text/FormattedStrings/FormattingCommandList.uc b/sources/Text/FormattedStrings/FormattingCommandList.uc deleted file mode 100644 index 17a779f..0000000 --- a/sources/Text/FormattedStrings/FormattingCommandList.uc +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Formatted string can be thought of as a string with a sequence of - * formatting-changing commands specified within it (either by opening new - * formatting block, swapping to color with "^" or by closing it and reverting - * to the previous one). - * This objects allows to directly access these commands. - * 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 FormattingCommandList extends AcediaObject - dependson(Text); - -enum FormattingCommandType -{ - // 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 ("{ ") we put another, -// current formatting on top of the stack; -// * Each time a section closes ("}") we pop the stack, returning to -// a previous formatting. -// * In a special case of "^" color swap that is supposed to last until -// current block closes we simply swap the color of the formatting on -// top of the stack. -struct FormattingCommand -{ - // Only defined for `FST_StackPush` commands that correspond to section - // openers ("{ "). - // Indices of first and last character belonging to block it opens. - var int openIndex; - var int closeIndex; - // Did this block start by opening or closing formatted part? - // Ignored for the very first block without any formatting. - var FormattingCommandType type; - // Full text inside the block, without any formatting - 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 Text.Character charTag; -}; -// Appending formatted `string` into the `MutableText` first requires its -// transformation into series of `FormattingCommand` and then their -// execution to assemble the `MutableText`. -// First element of `commandList` 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 private array commandList; - -// Stack that keeps track of which (by index inside `commandList`) command -// opened section we are currently parsing. This is needed to record positions -// at which each block is opened and closed. -var private array pushCommandIndicesStack; -// Store contents for the next command here, because appending array in -// the struct is expensive -var private array currentContents; -// `Parser` used to break input formatted string into commands, only used -// during building this object (inside `BuildSelf()` method). -var private Parser parser; -// `FormattingErrors` object used to reports errors during building process. -// It is "borrowed" - meaning that we do not really own it and should not -// deallocate it. Only set as a field for convenience. -var private FormattingErrors borrowedErrors; - -const CODEPOINT_ESCAPE = 27; // ASCII escape code -const CODEPOINT_OPEN_FORMAT = 123; // '{' -const CODEPOINT_CLOSE_FORMAT = 125; // '}' -const CODEPOINT_FORMAT_ESCAPE = 38; // '&' -const CODEPOINT_ACCENT = 94; // '^' -const CODEPOINT_TILDE = 126; // '~' - -protected function Finalizer() -{ - local int i; - _.memory.Free(parser); - parser = none; - borrowedErrors = none; - if (currentContents.length > 0) { - currentContents.length = 0; - } - if (pushCommandIndicesStack.length > 0) { - pushCommandIndicesStack.length = 0; - } - for (i = 0; i < commandList.length; i += 1) { - _.memory.Free(commandList[i].tag); - } - if (commandList.length > 0) { - commandList.length = 0; - } -} - -/** - * Create `FormattingCommandList` based on given `Text`. - * - * @param input `Text` that should be treated as "formatted" and - * to be broken into formatting commands. - * @param errorsReporter If specified, will be used to report errors - * (can only report `FSE_UnmatchedClosingBrackets`). - * @return New `FormattingCommandList` instance that allows us to have direct - * access to formatting commands. - */ -public final static function FormattingCommandList FromText( - Text input, - optional FormattingErrors errorsReporter) -{ - local FormattingCommandList newList; - newList = FormattingCommandList( - __().memory.Allocate(class'FormattingCommandList')); - newList.parser = __().text.Parse(input); - newList.borrowedErrors = errorsReporter; - newList.BuildSelf(); - __().memory.Free(newList.parser); - newList.parser = none; - newList.borrowedErrors = none; - return newList; -} - -/** - * Returns command with index `commandIndex`. - * - * @param commandIndex Index of the command to return. - * Must be non-negative (`>= 0`) and less than `GetAmount()`. - * @return Command with index `commandIndex`. - * If given `commandIndex` is out of bounds - returns invalid command. - * `tag` field is guaranteed to be non-`none` and should be deallocated. - */ -public final function FormattingCommand GetCommand(int commandIndex) -{ - local MutableText resultTag; - local FormattingCommand result; - if (commandIndex < 0) return result; - if (commandIndex >= commandList.length) return result; - - result = commandList[commandIndex]; - resultTag = result.tag; - if (resultTag != none) { - result.tag = resultTag.MutableCopy(); - } - return result; -} - -/** - * Returns amount of commands inside caller `FormattingCommandList`. - * - * @return Amount of commands inside caller `FormattingCommandList`. - */ -public final function int GetAmount() -{ - return commandList.length; -} - -// Method that turns `parser` into proper `FormattingCommandList` object. -private final function BuildSelf() -{ - //local int i; - local int characterCounter; - local Text.Character nextCharacter; - local FormattingCommand nextCommand; - while (!parser.HasFinished()) - { - parser.MCharacter(nextCharacter); - // New command by "{" - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) - { - nextCommand = AddCommand(nextCommand, FST_StackPush, characterCounter); - parser.MUntil(nextCommand.tag,, true) - .MCharacter(nextCommand.charTag); // Simply to skip a char - continue; - } - // New command by "}" - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) - { - nextCommand = AddCommand(nextCommand, FST_StackPop, characterCounter); - continue; - } - // New command by "^" - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) - { - nextCommand = AddCommand(nextCommand, FST_StackSwap, characterCounter); - parser.MCharacter(nextCommand.charTag); - if (!parser.Ok()) { - break; - } - continue; - } - // Escaped sequence - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { - parser.MCharacter(nextCharacter); - } - if (!parser.Ok()) { - break; - } - currentContents[currentContents.length] = nextCharacter; - characterCounter += 1; - } - // Only put in empty command if there is nothing else. - if (currentContents.length > 0 || commandList.length == 0) - { - nextCommand.contents = currentContents; - commandList[commandList.length] = nextCommand; - } - /*for (i = 0; i < commandList.length; i += 1) - { - Log(">>>COMMAND LIST FOR" @ i $ "<<<"); - Log("OPEN/CLOSE:" @ commandList[i].openIndex @ "/" @ commandList[i].closeIndex); - Log("TYPE:" @ commandList[i].type); - Log("CONTETS LENGTH:" @ commandList[i].contents.length); - if (commandList[i].tag != none) { - Log("TAG:" @ commandList[i].tag.ToString()); - } - else { - Log("TAG: NONE"); - } - }*/ -} - -// Helper method for a quick creation of a new `FormattingCommand` -private final function FormattingCommand AddCommand( - FormattingCommand nextCommand, - FormattingCommandType newStackCommandType, - optional int currentCharacterIndex) -{ - local int lastPushIndex; - local FormattingCommand newCommand; - nextCommand.contents = currentContents; - if (currentContents.length > 0) { - currentContents.length = 0; - } - commandList[commandList.length] = nextCommand; - if (newStackCommandType == FST_StackPop) - { - lastPushIndex = PopIndex(); - if (lastPushIndex >= 0) { - // BLABLA - commandList[lastPushIndex].closeIndex = currentCharacterIndex - 1; - } - else if (borrowedErrors != none) { - borrowedErrors.Report(FSE_UnmatchedClosingBrackets); - } - } - newCommand.type = newStackCommandType; - if (newStackCommandType == FST_StackPush) - { - newCommand.openIndex = currentCharacterIndex; - newCommand.closeIndex = -1; - // BLABLA - PushIndex(commandList.length); - } - return newCommand; -} - -private final function PushIndex(int index) -{ - pushCommandIndicesStack[pushCommandIndicesStack.length] = - commandList.length; -} - -private final function int PopIndex() -{ - local int result; - if (pushCommandIndicesStack.length <= 0) { - return -1; - } - result = pushCommandIndicesStack[pushCommandIndicesStack.length - 1]; - pushCommandIndicesStack.length = pushCommandIndicesStack.length - 1; - return result; -} - -defaultproperties -{ -} \ No newline at end of file diff --git a/sources/Text/FormattedStrings/FormattingCommandsSequence.uc b/sources/Text/FormattedStrings/FormattingCommandsSequence.uc new file mode 100644 index 0000000..63d3aa2 --- /dev/null +++ b/sources/Text/FormattedStrings/FormattingCommandsSequence.uc @@ -0,0 +1,316 @@ +/** + * Formatted string can be thought of as a string with a sequence of + * formatting-changing commands specified within it, along with raw contents + * to be pasted before performing next command (for more information about this + * see `FormattingStringParser`). This is a class for an accessor object + * that can return these individual commands based on the given `Text`/`string` + * (alongside with the construction code that determines these commands). + * This objects allows to directly access these commands. + * 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 FormattingCommandsSequence extends AcediaObject + dependson(Text); + +enum FormattingCommandType +{ + // Push more new formatting onto the stack. Corresponds to "{ ". + FST_StackPush, + // Pop formatting from the stack. Corresponds to "}". + FST_StackPop, + // Swap the top value on the formatting stack for a different formatting + // (pushes new one, if the stack is empty). Corresponds to "^". + FST_StackSwap +}; + +/** + * Represents formatting command + contents, alongside some additional + * meta information, necessary for `FormattingStringParser`. + */ +struct FormattingCommand +{ + var FormattingCommandType type; + var array contents; + + // Formatting character for the "^"-type tag + // This parameter is only used for `FST_StackSwap` command type. + var Text.Character charTag; + + // Rest of the parameters are only used for `FST_StackPush` + // command type. + // These commands correspond to section openers ("{ "): + // such openings define a *formatting block* between itself and matching + // closing curly braces "}". + // Meta information about these blocks is necessary for + // `FormattingStringParser`. + + // Formatting tag for the next block - "" from "{ ". + var MutableText tag; + // When formatting block for this command started and ended - + // necessary for gradient coloring. + // `closeIndex` should be equal to `-1` if it is not defined. + var int openIndex; + var int closeIndex; +}; +// All the commands we got from formatted string +var private array commandSequence; +// Store contents for the next command here, because constantly appending array +// inside the struct (`FormattingCommand` here) is expensive. +var private array currentContents; +// `Parser` used to break input formatted string into commands. +// It is only used during building this object (inside `BuildSelf()` method). +var private Parser parser; +// How many non-formatting defining characters we have parsed. +// That is characters that are actually meant to be displayed to the user +// and not the part of formatting definitions (e.g. "{$red", "}" or "^r"; +// "&{", "&&" or "&^" are also resolved into a single displayed character). +// `Parser`'s `GetParsedLength()` method is unusable here, since it +// reports all parsed characters. +var private int characterCounter; +// Command we are currently building. +// Making it a field makes code simpler and lets us avoid passing +// `FormattingCommand` struct between functions. +var private FormattingCommand currentCommand; +// `FormattingErrorsReport` we are given to report errors to. +// It is considered "borrowed": we do not really own it and will not +// deallocate it. +// Since, similar to `parser` field, it is only used during building this +// object - there is no danger of it being deallocated while we are storing +// this reference. +// Only set as a field for convenience, to avoid passing it as a parameter +// between methods during parsing. +var private FormattingErrorsReport borrowedErrors; + +// Stack that keeps track of which (by index inside `commandSequence`) command +// opened section we are currently parsing. This is needed to record positions +// at which each block is opened and closed. + +// It is easy to record opening indices for each formatting block by +// recording how many characters we have already processed before encountering +// opening statement "{ ". But to record closing indices we need to +// correspond correct opener with correct closer ("}"). +// We accomplish that by keeping track of all formatted blocks opened +// at the current moment during parsing in a stack and popping the top value +// upon reaching "}". +// We identify each formatting block by recording index of corresponding +// `FormattingCommand` inside `commandSequence` array. This makes setting +// appropriate `closeIndex` simple. +var private array pushCommandIndicesStack; + +const CODEPOINT_OPEN_FORMAT = 123; // '{' +const CODEPOINT_CLOSE_FORMAT = 125; // '}' +const CODEPOINT_FORMAT_ESCAPE = 38; // '&' +const CODEPOINT_ACCENT = 94; // '^' + +protected function Finalizer() +{ + local int i; + for (i = 0; i < commandSequence.length; i += 1) { + _.memory.Free(commandSequence[i].tag); + } + pushCommandIndicesStack.length = 0; + currentContents.length = 0; + commandSequence.length = 0; + characterCounter = 0; + // These fields should not be set at this point, but clean them up + // just in case + _.memory.Free(parser); + parser = none; + borrowedErrors = none; +} + +/** + * Create `FormattingCommandsSequence` based on the given `Text`. + * + * There is not separate method for `string`, since we would require reading it + * into a `Text` as a *plain string* first anyway and, as this class is + * technical/internal, no convenience methods are needed. + * + * @param input `Text` that should be treated as "formatted" and + * to be broken into formatting commands. + * @param errorsReporter If specified, will be used to report errors detected + * during construction of `FormattingCommandsSequence` + * (can only report `FSE_UnmatchedClosingBrackets`). + * @return New `FormattingCommandsSequence` instance that allows us to have + * direct access to formatting commands defined in `input`. + */ +public final static function FormattingCommandsSequence FromText( + Text input, + optional FormattingErrorsReport errorsReporter) +{ + local FormattingCommandsSequence newSequence; + newSequence = FormattingCommandsSequence( + __().memory.Allocate(class'FormattingCommandsSequence')); + // Setup variables + newSequence.parser = __().text.Parse(input); + newSequence.borrowedErrors = errorsReporter; + // Parse + newSequence.BuildSelf(); + // Clean up + __().memory.Free(newSequence.parser); + newSequence.parser = none; + newSequence.borrowedErrors = none; + return newSequence; +} + +/** + * Amount of commands to reconstruct formatted string caller + * `FormattingCommandsSequence` was created from. + * + * @return Amount of commands inside caller `FormattingCommandsSequence`. + */ +public final function int GetAmount() +{ + return commandSequence.length; +} + +/** + * Returns command with index `commandIndex`. Indexation starts from `0`. + * + * @param commandIndex Index of the command to return. + * Must be non-negative (`>= 0`) and less than `GetAmount()`. + * @return Command with index `commandIndex`. + * If given `commandIndex` is out of bounds - returns invalid command. + * `tag` field is guaranteed to be non-`none` for commands of type + * `FST_StackPush` and should be deallocated, as per usual rules. + */ +public final function FormattingCommand GetCommand(int commandIndex) +{ + local MutableText resultTag; + local FormattingCommand result; + if (commandIndex < 0) return result; + if (commandIndex >= commandSequence.length) return result; + + result = commandSequence[commandIndex]; + resultTag = result.tag; + if (resultTag != none) { + result.tag = resultTag.MutableCopy(); + } + return result; +} + +private final function BuildSelf() +{ + local Text.Character nextCharacter; + while (!parser.HasFinished()) + { + parser.MCharacter(nextCharacter); + // New command by "{ " + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) + { + AddCommand(FST_StackPush); + parser + .MUntil(currentCommand.tag,, true) + .MCharacter(currentCommand.charTag); // Simply to skip a char + continue; + } + // New command by "}" + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) + { + AddCommand(FST_StackPop); + continue; + } + // New command by "^" + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) + { + AddCommand(FST_StackSwap); + parser.MCharacter(currentCommand.charTag); + if (!parser.Ok()) { + break; + } + continue; + } + // Escaped sequence + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { + parser.MCharacter(nextCharacter); + } + if (!parser.Ok()) { + break; + } + currentContents[currentContents.length] = nextCharacter; + characterCounter += 1; + } + // Only put in empty command if there is nothing else + if (currentContents.length > 0 || commandSequence.length == 0) + { + currentCommand.contents = currentContents; + commandSequence[commandSequence.length] = currentCommand; + } + // We no longer use `currentCommand` and have transferred ownership over + // `currentCommand.tag` to the `commandSequence`, so better forget about it + // to avoid messing up. + currentCommand.tag = none; +} + +// Helper method for a adding `currentCommand` to the command sequence and +// quick creation of a new `FormattingCommand` in its place +private final function AddCommand(FormattingCommandType newStackCommandType) +{ + local int lastPushIndex; + local int lastCharacterIndex; + local FormattingCommand newCommand; + currentCommand.contents = currentContents; + currentContents.length = 0; + commandSequence[commandSequence.length] = currentCommand; + // Last (so far) character index in a string equals total amount of + // parsed characters minus one + lastCharacterIndex = characterCounter - 1; + if (newStackCommandType == FST_StackPop) + { + lastPushIndex = PopIndex(); + if (lastPushIndex >= 0) { + commandSequence[lastPushIndex].closeIndex = lastCharacterIndex; + } + else if (borrowedErrors != none) { + borrowedErrors.Report(FSE_UnmatchedClosingBrackets); + } + } + newCommand.type = newStackCommandType; + if (newStackCommandType == FST_StackPush) + { + // Formatting should be applied to the next character, + // not the currently last added one + newCommand.openIndex = lastCharacterIndex + 1; + newCommand.closeIndex = -1; + // `FormattingCommand` that new formatting block corresponds to + // is not added yet, but it is guaranteed to be added next, + // we know its future index + PushIndex(commandSequence.length); + } + currentCommand = newCommand; +} + +private final function int PopIndex() +{ + local int result; + if (pushCommandIndicesStack.length <= 0) { + return -1; + } + result = pushCommandIndicesStack[pushCommandIndicesStack.length - 1]; + pushCommandIndicesStack.length = pushCommandIndicesStack.length - 1; + return result; +} + +private final function PushIndex(int index) +{ + pushCommandIndicesStack[pushCommandIndicesStack.length] = + commandSequence.length; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Text/FormattedStrings/FormattingErrors.uc b/sources/Text/FormattedStrings/FormattingErrorsReport.uc similarity index 68% rename from sources/Text/FormattedStrings/FormattingErrors.uc rename to sources/Text/FormattedStrings/FormattingErrorsReport.uc index 1e26256..39aca14 100644 --- a/sources/Text/FormattedStrings/FormattingErrors.uc +++ b/sources/Text/FormattedStrings/FormattingErrorsReport.uc @@ -18,12 +18,12 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class FormattingErrors extends AcediaObject; +class FormattingErrorsReport extends AcediaObject; /** * Errors that can occur during parsing of the formatted string. */ -enum FormattedDataErrorType +enum FormattedStringErrorType { // There was an unmatched closing figure bracket, e.g. // "{$red Hey} you, there}!" @@ -34,9 +34,11 @@ enum FormattedDataErrorType // e.g. "Why not {just kill them}?" FSE_BadColor, // Gradient color tag contained bad point specified, e.g. - // "That is SO {$red~$orange(what?)~$red AMAZING}!!!" or - // "That is SO {$red~$orange(0.76~$red AMAZING}!!!" + // "That is SO {$red:$orange[what?]:$red AMAZING}!!!" or + // "That is SO {$red:$orange[0.76:$red AMAZING}!!!" FSE_BadGradientPoint, + // Short tag (e.g. "^r" or "^2") was specified, but the character after "^" + // is not configured to correspond to any color FSE_BadShortColorTag }; @@ -45,26 +47,29 @@ enum FormattedDataErrorType // invoked. var private int unmatchedClosingBracketsErrorCount; var private int emptyColorTagErrorCount; -// `FSE_BadColor` and `FSE_BadGradientPoint` are always expected to have -// a `Text` hint reported alongside them, so simply store that hint. +// `FSE_BadColor`, `FSE_BadGradientPoint` and `FSE_BadShortColorTag` are always +// expected to have a `Text` hint reported alongside them. We store that hint. var private array badColorTagErrorHints; var private array badGradientTagErrorHints; var private array badShortColorTagErrorHints; -// We will report accumulated errors as an array of these structs. -struct FormattedDataError +/** + * `FormattingErrorsReport` returns reported errors in formatting strings via + * this struct. + */ +struct FormattedStringError { // Type of the error - var FormattedDataErrorType type; + var FormattedStringErrorType type; // How many times had this error happened? // Can be specified for `FSE_UnmatchedClosingBrackets` and // `FSE_EmptyColorTag` error types. Never negative. - var int count; + var int count; // `Text` hint that should help user understand where the error is // coming from. - // Can be specified for `FSE_BadColor` and `FSE_BadGradientPoint` - // error types. - var Text cause; + // Can be specified for `FSE_BadColor`, `FSE_BadGradientPoint` and + // `FSE_BadShortColorTag` error types. + var Text cause; }; protected function Finalizer() @@ -80,15 +85,17 @@ protected function Finalizer() } /** - * Adds new error to the caller `FormattingErrors` object. + * Adds new error to the caller `FormattingErrorsReport` object. * * @param type Type of the new error. * @param cause Auxiliary `Text` that might give user additional hint about * what exactly went wrong. - * If this parameter is `none` for errors `FSE_BadColor` or - * `FSE_BadGradientPoint` - method will do nothing. + * If this parameter is `none` for errors of type `FSE_BadColor`, + * `FSE_BadGradientPoint` or `FSE_BadShortColorTag`, then method will + * do nothing. + * Parameter is unused for other types of errors. */ -public final function Report(FormattedDataErrorType type, optional Text cause) +public final function Report(FormattedStringErrorType type, optional Text cause) { switch (type) { @@ -121,21 +128,37 @@ public final function Report(FormattedDataErrorType type, optional Text cause) } /** - * Returns array of errors collected so far. + * Returns all formatted string errors reported for caller + * `FormattingErrorReport`. * - * @return Array of errors collected so far. - * Each `FormattedDataError` in array has either non-`none` `cause` field - * or strictly positive `count > 0` field (but not both). - * `count` field is always guaranteed to not be negative. - * WARNING: `FormattedDataError` struct may contain `Text` objects that - * should be deallocated. + * @return Array of `FormattedStringError`s that represent reported errors. + * Each `FormattedStringError` item in array has either: + * * non-`none` `cause` field or; + * * strictly positive `count > 0` field. + * But never both. + * `count` field is always guaranteed to be non-negative. + * WARNING: `FormattedStringError` struct may contain `Text` objects that + * should be deallocated, as per usual rules. */ -public final function array GetErrors() +public final function array GetErrors() { - local int i; - local FormattedDataError newError; - local array errors; - // We overwrite old `cause` in `newError` with new one each time we + local int i; + local FormattedStringError newError; + local array errors; + // First add errors that do not need `cause` variable + if (unmatchedClosingBracketsErrorCount > 0) + { + newError.type = FSE_UnmatchedClosingBrackets; + newError.count = unmatchedClosingBracketsErrorCount; + errors[errors.length] = newError; + } + if (emptyColorTagErrorCount > 0) + { + newError.type = FSE_EmptyColorTag; + newError.count = emptyColorTagErrorCount; + errors[errors.length] = newError; + } + // We overwrite old `newError.cause` with new `Text` object each time we // add new error, so it should be fine to not set it to `none` after // "moving it" into `errors`. newError.type = FSE_BadColor; @@ -156,21 +179,6 @@ public final function array GetErrors() newError.cause = badGradientTagErrorHints[i].Copy(); errors[errors.length] = newError; } - // Need to reset `cause` here, to avoid duplicating it in - // following two errors - newError.cause = none; - if (unmatchedClosingBracketsErrorCount > 0) - { - newError.type = FSE_UnmatchedClosingBrackets; - newError.count = unmatchedClosingBracketsErrorCount; - errors[errors.length] = newError; - } - if (emptyColorTagErrorCount > 0) - { - newError.type = FSE_EmptyColorTag; - newError.count = emptyColorTagErrorCount; - errors[errors.length] = newError; - } return errors; } diff --git a/sources/Text/FormattedStrings/FormattingStringParser.uc b/sources/Text/FormattedStrings/FormattingStringParser.uc new file mode 100644 index 0000000..e4520b1 --- /dev/null +++ b/sources/Text/FormattedStrings/FormattingStringParser.uc @@ -0,0 +1,548 @@ +/** + * A simple parser with a single public method for parsing formatted strings. + * Was introduced instead of a simple method in `MutableText` to: + * 1. Allow for reporting errors caused by badly specified colors; + * 2. Allow for a more complicated case of specifying a color gradient + * range. + * 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 FormattingStringParser extends AcediaObject + dependson(Text) + dependson(FormattingErrorsReport) + dependson(FormattingCommandsSequence); + +/** + * # Usage + * + * Public interface of this parser consists of a single static method + * `ParseFormatted()` that temporarily creates (and auto deallocates before + * method has returned) instance of its own class to store the state necessary + * during parsing. + * + * # Implementation + * + * ## Formatting commands + * + * The algorithm looks at formatting block "{ ...}" as a set + * of two operations: turn on a certain formatting ("{") and + * turn it off ("}"). Since blocks can be folded into each other, as we parse + * the string we put opened ones onto the stack, closing them upon + * encountering "}". Short color tags "^" are handled by + * switching formatting within the current block. + * Overall this leads us to transforming markdown of formatted string into + * sequence of three operations: + * 1. Put formatting onto the formatting stack (and add following + * contents); + * 2. Pop formatting off the formatting stack (and add following contents); + * 3. Swap formatting on top of the formatting stack (and add following + * contents). + * Transforming formatted string in such a sequence is moved out from this + * class into auxiliary `FormattingCommandsSequence`, since it task is + * logically separated from the one that comes next... + * + * ## Building `MutableText` + * + * Once we have broken formatted string down into sequence of formatted + * commands, we only need to go through them, command-by-command, appending + * their contents to the resulting `MutableText` with formatting, specified by + * the command. + * The only somewhat complicated part here are formatted blocks with + * specified gradient coloring + * ("::[]:"). For that we make use + * of the information about starting and ending indices of gradient formatted + * block, given by `FormattingCommandsSequence`: + * * We correct/uniformly place missing points where each intermediate + * color must be at 100% on the segment [0; 1], where 0 represents + * start of the formatting block and 1 its end; + * * Then for each character we determine between which color it lies and + * how far from each of them (again on the scale from 0 to 1, + * where `0` is left color and `1` is right color); + * * Finally we use linear interpolation between selected pair of colors to + * determine appropriate formatting. + */ + +/** + * Element of the formatting stack, that completely defines formatting block. + */ +struct FormattingInfo +{ + // Is segment even colored? + var bool colored; + // Does it use color gradient? + var bool gradient; + // Color of the segment, only used when `gradient` equals `true` + var Color plainColor; + // All the colors for gradient inside the segment, only used when + // `gradient` equals `false` + var array gradientColors; + // Points (from 0 to 1) at which each `gradientColors` with the same index + // should be at its 100% + var array gradientPoints; + // To decide how to color each character we need to know position of + // the segment, it is convenient for us to store it as a starting point + // and length. + // Length is stored as `float` because it will mostly be used as + // a divisor of some values and we need a `float` result. + var int gradientStart; + var float gradientLength; +}; +// 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 private array formattingStack; +// Keep top element copied into a separate variable for quicker access. +// Must maintain invariant: if `formattingStack.length > 0` +// then `formattingStack[formattingStack.length - 1] == formattingStackHead`. +var private FormattingInfo formattingStackHead; +// For calculating gradient we need to know what character we are +// currently adding. +var private int nextCharacterIndex; +// `FormattingStringParser` itself only performs "stage 2" of the algorithm, +// while "stage 1" (converting formatted string into a sequence of commands is +// done by this object). +var private FormattingCommandsSequence commandSequence; +// Text we are appending formatted string to +var private MutableText borrowedTarget; +var private FormattingErrorsReport borrowedErrors; + +// Keep this as an easy access to separator of gradient colors ':' +var private Text.Character separatorCharacter; + +var private const int TOPENING_BRACKET, TCLOSING_BRACKET, TPERCENT; + +protected function Constructor() +{ + separatorCharacter = _.text.GetCharacter(":"); +} + +protected function Finalizer() +{ + formattingStack.length = 0; + _.memory.Free(commandSequence); // the only object we have owned + commandSequence = none; + borrowedTarget = none; + borrowedErrors = none; +} + +/** + * Parses formatted string given by the `source`. + * + * As a result of parsing can either append it to given `MutableText` or + * report any errors in its formatting. + * + * @param source `Text` to parse as a formatted string. + * @param target Method will append result of parsing `source` into + * this parameter. Does nothing if it is equal to `none`. + * @param doReportErrors Set this to `true` if you want parsing errors to be + * reported in the return value and `false` otherwise. + * @return Array of formatting errors in the given `source` formatted string, + * each represented by `FormattedStringError` struct. + * Errors are only generated if `doReportErrors` is equals to `true`. + * If `doReportErrors` is `false`, then returned value is guaranteed to be + * an empty array. + * Each `FormattedStringError` item in array has either: + * * non-`none` `cause` field or; + * * strictly positive `count > 0` field. + * But never both. + * `count` field is always guaranteed to be non-negative. + * WARNING: `FormattedStringError` struct may contain `Text` objects that + * should be deallocated, as per usual rules. + */ +public static final function array + ParseFormatted( + Text source, + optional MutableText target, + optional bool doReportErrors) +{ + local FormattingErrorsReport newErrorsReport; + local FormattingStringParser newFormattingParser; + local array resultErrors; + if (source == none) return resultErrors; + if (target == none && !doReportErrors) return resultErrors; + + // Setup formatting parser + newFormattingParser = FormattingStringParser(__().memory + .Allocate(class'FormattingStringParser')); + if (doReportErrors) + { + newErrorsReport = FormattingErrorsReport(__().memory + .Allocate(class'FormattingErrorsReport')); + newFormattingParser.borrowedErrors = newErrorsReport; + } + newFormattingParser.commandSequence = + class'FormattingCommandsSequence'.static + .FromText(source, newErrorsReport); + newFormattingParser.borrowedTarget = target; + // Do it and release resources + newFormattingParser.DoAppend(); + // We have only set these fields for access convenience and we + // neither own `target` that will contain appended formatted string, + // nor errors report that user requires, so release them right after use + newFormattingParser.borrowedTarget = none; + newFormattingParser.borrowedErrors = none; + __().memory.Free(newFormattingParser); + if (newErrorsReport != none) + { + resultErrors = newErrorsReport.GetErrors(); + __().memory.Free(newErrorsReport); + } + return resultErrors; +} + +private final function DoAppend() +{ + local int i; + local Text.Formatting emptyFormatting; + local FormattingCommandsSequence.FormattingCommand nextCommand; + SetupFormattingStack(emptyFormatting); + // First element of color stack is special and has no color information; + // see `BuildFormattingStackCommands()` for details. + nextCommand = commandSequence.GetCommand(0); + // First block is always not formatted + if (borrowedTarget != none) { + borrowedTarget.AppendManyRawCharacters(nextCommand.contents); + } + nextCharacterIndex = nextCommand.contents.length; + _.memory.Free(nextCommand.tag); + for (i = 1; i < commandSequence.GetAmount(); i += 1) + { + nextCommand = commandSequence.GetCommand(i); + if (nextCommand.type == FST_StackPush) { + PushIntoFormattingStack(nextCommand); + } + else if (nextCommand.type == FST_StackPop) { + PopFormattingStack(); + } + else if (nextCommand.type == FST_StackSwap) { + SwapFormattingStack(nextCommand.charTag); + } + _.memory.Free(nextCommand.tag); + if (borrowedTarget != none) { + AppendToTarget(nextCommand.contents); + } + } +} + +// Auxiliary method for appending `contents` character with an appropriate +// formatting and parser's state modification. +private final function AppendToTarget(array contents) +{ + local int i; + if (!IsCurrentFormattingGradient()) + { + borrowedTarget.AppendManyRawCharacters( + contents, + GetFormattingFor(nextCharacterIndex)); + nextCharacterIndex += contents.length; + return; + } + for (i = 0; i < contents.length; i += 1) + { + borrowedTarget.AppendRawCharacter( + contents[i], + GetFormattingFor(nextCharacterIndex)); + nextCharacterIndex += 1; + } +} + +private final function Report( + FormattingErrorsReport.FormattedStringErrorType type, + optional Text cause) +{ + if (borrowedErrors == none) { + return; + } + borrowedErrors.Report(type, cause); +} + +private final function bool IsCurrentFormattingGradient() +{ + if (formattingStack.length <= 0) { + return false; + } + return formattingStackHead.gradient; +} + +private final function Text.Formatting GetFormattingFor(int index) +{ + local Text.Formatting emptyFormatting; + if (formattingStack.length <= 0) return emptyFormatting; + if (!formattingStackHead.colored) return emptyFormatting; + + return _.text.FormattingFromColor(GetColorFor(index)); +} + +private final function Color GetColorFor(int index) +{ + local int i; + local float indexPosition, leftPosition, rightPosition; + local array points; + local Color leftColor, rightColor, targetColor; + if (formattingStack.length <= 0) return targetColor; + if (!formattingStackHead.gradient) return formattingStackHead.plainColor; + + indexPosition = float(index - formattingStackHead.gradientStart) / + formattingStackHead.gradientLength; + points = formattingStackHead.gradientPoints; + for (i = 1; i < points.length; i += 1) + { + if (points[i - 1] <= indexPosition && indexPosition <= points[i]) + { + leftPosition = points[i - 1]; + rightPosition = points[i]; + leftColor = formattingStackHead.gradientColors[i - 1]; + rightColor = formattingStackHead.gradientColors[i]; + break; + } + } + indexPosition = + (indexPosition - leftPosition) / (rightPosition - leftPosition); + targetColor.R = Lerp(indexPosition, leftColor.R, rightColor.R); + targetColor.G = Lerp(indexPosition, leftColor.G, rightColor.G); + targetColor.B = Lerp(indexPosition, leftColor.B, rightColor.B); + targetColor.A = Lerp(indexPosition, leftColor.A, rightColor.A); + return targetColor; +} + +private final function FormattingInfo ParseFormattingInfo(Text colorTag) +{ + local int i; + local Parser colorParser; + local Color nextColor; + local array specifiedColors; + local array gradientColors; + local array gradientPoints; + local FormattingInfo targetInfo; + if (colorTag.IsEmpty()) + { + Report(FSE_EmptyColorTag); + return targetInfo; // not colored + } + specifiedColors = colorTag.SplitByCharacter(separatorCharacter, true); + for (i = 0; i < specifiedColors.length; i += 1) + { + colorParser = _.text.Parse(specifiedColors[i]); + if (_.color.ParseWith(colorParser, nextColor)) + { + colorParser.Confirm(); + gradientColors[gradientColors.length] = nextColor; + gradientPoints[gradientPoints.length] = ParsePoint(colorParser); + } + else { + Report(FSE_BadColor, specifiedColors[i]); + } + _.memory.Free(colorParser); + } + _.memory.FreeMany(specifiedColors); + gradientPoints = NormalizePoints(gradientPoints); + targetInfo.colored = (gradientColors.length > 0); + targetInfo.gradient = (gradientColors.length > 1); + targetInfo.gradientColors = gradientColors; + targetInfo.gradientPoints = gradientPoints; + if (gradientColors.length > 0) { + targetInfo.plainColor = gradientColors[0]; + } + return targetInfo; +} + +private final function float ParsePoint(Parser parser) +{ + local float point; + local Parser.ParserState initialState; + if (!parser.Ok() || parser.HasFinished()) { + return -1; + } + initialState = parser.GetCurrentState(); + // [Necessary part] Should starts with "[" + if (!parser.Match(T(TOPENING_BRACKET)).Ok()) + { + Report( + FSE_BadGradientPoint, + parser.RestoreState(initialState).GetRemainder()); + return -1; + } + // [Necessary part] Try parsing number + parser.MNumber(point).Confirm(); + if (!parser.Ok()) + { + Report( + FSE_BadGradientPoint, + parser.RestoreState(initialState).GetRemainder()); + return -1; + } + // [Optional part] Check if number is a percentage + if (parser.Match(T(TPERCENT)).Ok()) { + point *= 0.01; + } + // This either confirms state of parsing "%" (on success) + // or reverts to the previous state, just after parsing the number + // (on failure) + parser.Confirm(); + parser.R(); + // [Necessary part] Have to have closing parenthesis + if (!parser.HasFinished()) { + parser.Match(T(TCLOSING_BRACKET)).Confirm(); + } + // Still return `point`, even if there was no closing parenthesis, + // since that is likely what user wants + if (!parser.Ok()) + { + Report( + FSE_BadGradientPoint, + parser.RestoreState(initialState).GetRemainder()); + } + return point; +} + +private final function array NormalizePoints(array points) +{ + local int i, j; + local int negativeSegmentStart, negativeSegmentLength; + local float leftPositiveBound, rightPositiveBound; + local bool foundNegative; + // Leftmost and rightmost points are always fixed + if (points.length > 1) + { + points[0] = 0.0; + points[points.length - 1] = 1.0; + } + for (i = 1; i < points.length - 1; i += 1) + { + // Each point must be in bounds (between `0` and `1`) and points + // must be specified in an increasing order. + // If either does not hold - simply mark point as unspecified and + // let let it be regenerated naturally. + if (points[i] <= 0 || points[i] > 1 || points[i] <= points[i - 1]) { + points[i] = -1; + } + } + // Check all points - if a sequence of them are undefined, then place + // them uniformly between bounding non-negative points. + // For example [0.5, -1, -1, -1, -1, 1] should turn into + // [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]. + // NOTE: at the beginning of this method we have forced `points[0]` + // to be `0.0` and `points[points.length - 1]` to be `1.0`. Thanks to that + // there always exists left and right non-negative bounding points. + for (i = 1; i < points.length; i += 1) + { + // Found first element of negative sequence + if (!foundNegative && points[i] < 0) + { + leftPositiveBound = points[i - 1]; + negativeSegmentStart = i; + } + // Found where negative sequence ends + if (foundNegative && points[i] > 0) + { + rightPositiveBound = points[i]; + for (j = negativeSegmentStart; j < i; j += 1) + { + points[j] = Lerp( + float(j - negativeSegmentStart + 1) / + float(negativeSegmentLength + 1), + leftPositiveBound, + rightPositiveBound); + } + negativeSegmentLength = 0; + } + foundNegative = (points[i] < 0); + // Still continuing with negative segment + if (foundNegative) { + negativeSegmentLength += 1; + } + } + return points; +} + +// 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. +private final function SetupFormattingStack(Text.Formatting defaultFormatting) +{ + local FormattingInfo defaultFormattingInfo; + defaultFormattingInfo.colored = defaultFormatting.isColored; + defaultFormattingInfo.plainColor = defaultFormatting.color; + if (formattingStack.length > 0) { + formattingStack.length = 0; + } + formattingStack[0] = defaultFormattingInfo; + formattingStackHead = defaultFormattingInfo; +} + +private final function PushIntoFormattingStack( + FormattingCommandsSequence.FormattingCommand formattingCommand) +{ + formattingStackHead = ParseFormattingInfo(formattingCommand.tag); + formattingStackHead.gradientStart = formattingCommand.openIndex; + formattingStackHead.gradientLength = + float(formattingCommand.closeIndex - formattingCommand.openIndex); + formattingStack[formattingStack.length] = formattingStackHead; +} + +private final function SwapFormattingStack(Text.Character tagCharacter) +{ + local FormattingInfo updatedFormatting; + if (formattingStack.length > 0) { + updatedFormatting = formattingStackHead; + } + if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.plainColor)) + { + updatedFormatting.colored = true; + updatedFormatting.gradient = false; + } + else + { + Report( + FSE_BadShortColorTag, + _.text.FromString("^" $ Chr(tagCharacter.codePoint))); + } + formattingStackHead = updatedFormatting; + if (formattingStack.length > 0) { + formattingStack[formattingStack.length - 1] = updatedFormatting; + } + else { + formattingStack[0] = updatedFormatting; + } +} + +private final function PopFormattingStack() +{ + // Remove the top of the stack + if (formattingStack.length > 0) { + formattingStack.length = formattingStack.length - 1; + } + // Update the stack head copy + if (formattingStack.length > 0) { + formattingStackHead = formattingStack[formattingStack.length - 1]; + } +} + +defaultproperties +{ + TOPENING_BRACKET = 0 + stringConstants(0) = "[" + TCLOSING_BRACKET = 1 + stringConstants(1) = "]" + TPERCENT = 2 + stringConstants(2) = "%" +} \ No newline at end of file diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index dca84d3..b12bfd8 100644 --- a/sources/Text/MutableText.uc +++ b/sources/Text/MutableText.uc @@ -273,43 +273,29 @@ public final function MutableText AppendColoredString( /** * Appends contents of the formatted `Text` to the caller `MutableText`. * - * @param source `Text` (with formatted string contents) to be + * @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) { - // TODO: is this the best way? - local Text appendedPart; - local FormattedStringData data; - data = class'FormattedStringData'.static.FromText(source); - appendedPart = data.GetResult(); - Append(appendedPart); - _.memory.Free(appendedPart); - _.memory.Free(data); + class'FormattingStringParser'.static.ParseFormatted(source, self); return self; } /** * Appends contents of the formatted `string` to the caller `MutableText`. * - * @param source Formatted `string` to be appended to + * @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. */ public final function MutableText AppendFormattedString( string source, optional Formatting defaultFormatting) { - // TODO: is this the best way? local Text sourceAsText; sourceAsText = _.text.FromString(source); AppendFormatted(sourceAsText); diff --git a/sources/Text/Tests/TEST_FormattedStrings.uc b/sources/Text/Tests/TEST_FormattedStrings.uc index caeeee3..23f6aa9 100644 --- a/sources/Text/Tests/TEST_FormattedStrings.uc +++ b/sources/Text/Tests/TEST_FormattedStrings.uc @@ -57,207 +57,6 @@ protected static function TESTS() Test_Errors(); } -protected static function Test_Errors() -{ - Context("Testing error reporting for formatted strings."); - SubTest_ErrorUnmatchedClosingBrackets(); - SubTest_ErrorEmptyColorTag(); - SubTest_ErrorBadColor(); - SubTest_ErrorBadShortColorTag(); - SubTest_ErrorBadGradientPoint(); - SubTest_AllErrors(); -} - -protected static function SubTest_ErrorUnmatchedClosingBrackets() -{ - local array errors; - local FormattedStringData data; - Issue("Unmatched closing brackets are not reported."); - data = class'FormattedStringData'.static.FromText(P("Testing {$pink pink text}}!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); - TEST_ExpectTrue(errors[0].count == 1); - TEST_ExpectNone(errors[0].cause); - data = class'FormattedStringData'.static.FromText(P("Testing regular text!}"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); - TEST_ExpectTrue(errors[0].count == 1); - TEST_ExpectNone(errors[0].cause); - data = class'FormattedStringData'.static - .FromText(P("This is {rgb(255,0,0) red{rgb(255,255,255) , }}}" - $ "{rgb(0,255,0) gr}een{rgb(255,255,255) and }}}}{rgb(0,0,255)" - $ " blue!}}}"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); - TEST_ExpectTrue(errors[0].count == 6); - TEST_ExpectNone(errors[0].cause); -} - -protected static function SubTest_ErrorEmptyColorTag() -{ - local array errors; - local FormattedStringData data; - Issue("Empty color tags are not reported."); - data = class'FormattedStringData'.static.FromText(P("Testing { pink text}!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); - TEST_ExpectTrue(errors[0].count == 1); - TEST_ExpectNone(errors[0].cause); - data = class'FormattedStringData'.static.FromText(P("Testing {$red regu{ lar tex}t!}"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); - TEST_ExpectTrue(errors[0].count == 1); - TEST_ExpectNone(errors[0].cause); - data = class'FormattedStringData'.static - .FromText(P("This is { {rgb(255,255,255)~$green , }" - $ "{#800c37 ^ggre^gen{ and }}}^bblue!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); - TEST_ExpectTrue(errors[0].count == 2); - TEST_ExpectNone(errors[0].cause); -} - -protected static function SubTest_ErrorBadColor() -{ - local array errors; - local FormattedStringData data; - Issue("Bad color is not reported."); - data = class'FormattedStringData'.static.FromText(P("Testing {$cat pink text}!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_BadColor); - TEST_ExpectTrue(errors[0].cause.ToString() == "$cat"); - TEST_ExpectTrue(errors[0].count == 0); - data = class'FormattedStringData'.static.FromText(P("Testing {dog regular} {#wicked text!}"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 2); - TEST_ExpectTrue(errors[0].type == FSE_BadColor); - TEST_ExpectTrue(errors[1].type == FSE_BadColor); - TEST_ExpectTrue(errors[0].cause.ToString() == "dog"); - TEST_ExpectTrue(errors[1].cause.ToString() == "#wicked"); - data = class'FormattedStringData'.static - .FromText(P("This is {goat red{rgb(255,255,255)~lol~$green , }" - $ "{#800c37 ^ggre^gen{324sd and }}}^bblue!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 3); - TEST_ExpectTrue(errors[0].type == FSE_BadColor); - TEST_ExpectTrue(errors[1].type == FSE_BadColor); - TEST_ExpectTrue(errors[2].type == FSE_BadColor); - TEST_ExpectTrue(errors[0].cause.ToString() == "goat"); - TEST_ExpectTrue(errors[1].cause.ToString() == "lol"); - TEST_ExpectTrue(errors[2].cause.ToString() == "324sd"); -} - -protected static function SubTest_ErrorBadShortColorTag() -{ - local array errors; - local FormattedStringData data; - Issue("Bad short color tag is not reported."); - data = class'FormattedStringData'.static.FromText(P("This is ^xred^w, ^ugreen^x and ^zblue!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 4); - TEST_ExpectTrue(errors[0].type == FSE_BadShortColorTag); - TEST_ExpectTrue(errors[0].cause.ToString() == "^x"); - TEST_ExpectTrue(errors[0].count == 0); - TEST_ExpectTrue(errors[1].type == FSE_BadShortColorTag); - TEST_ExpectTrue(errors[1].cause.ToString() == "^u"); - TEST_ExpectTrue(errors[1].count == 0); - TEST_ExpectTrue(errors[2].type == FSE_BadShortColorTag); - TEST_ExpectTrue(errors[2].cause.ToString() == "^x"); - TEST_ExpectTrue(errors[2].count == 0); - TEST_ExpectTrue(errors[3].type == FSE_BadShortColorTag); - TEST_ExpectTrue(errors[3].cause.ToString() == "^z"); - TEST_ExpectTrue(errors[3].count == 0); -} - -protected static function SubTest_ErrorBadGradientPoint() -{ - local array errors; - local FormattedStringData data; - Issue("Bad gradient point is not reported."); - data = class'FormattedStringData'.static.FromText(P("Testing {$pink[dog] pink text}!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 1); - TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); - TEST_ExpectTrue(errors[0].cause.ToString() == "[dog]"); - TEST_ExpectTrue(errors[0].count == 0); - data = class'FormattedStringData'.static.FromText(P("Testing {45,2,241[bad] regular} {#ffaacd~rgb(2,3,4)45worse] text!}"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 2); - TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); - TEST_ExpectTrue(errors[1].type == FSE_BadGradientPoint); - TEST_ExpectTrue(errors[0].cause.ToString() == "[bad]"); - TEST_ExpectTrue(errors[1].cause.ToString() == "45worse]"); - data = class'FormattedStringData'.static - .FromText(P("This is {$red[45%%] red{rgb(255,255,255)~45,3,128point~$green , }" - $ "{#800c37 ^ggre^gen{#43fa6b3c and }}}^bblue!"), true); - errors = data.BorrowErrors().GetErrors(); - TEST_ExpectTrue(errors.length == 3); - TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); - TEST_ExpectTrue(errors[1].type == FSE_BadGradientPoint); - TEST_ExpectTrue(errors[2].type == FSE_BadGradientPoint); - TEST_ExpectTrue(errors[0].cause.ToString() == "[45%%]"); - TEST_ExpectTrue(errors[1].cause.ToString() == "point"); - TEST_ExpectTrue(errors[2].cause.ToString() == "3c"); -} - -protected static function SubTest_AllErrors() -{ - local int i; - local bool foundUnmatched, foundEmpty, foundBadColor, foundBadPoint, foundBadShortTag; - local array errors; - local FormattedStringData data; - Issue("COMPLEX."); - data = class'FormattedStringData'.static.FromText(P("This} is {$cat~$green[%7] red{$white , }{ green^z and }}{$blue blue!}}"), true); - errors = data.BorrowErrors().GetErrors(); - for (i = 0; i < errors.length; i += 1) - { - if (errors[i].type == FSE_UnmatchedClosingBrackets) - { - foundUnmatched = true; - TEST_ExpectTrue(errors[i].count == 2); - } - if (errors[i].type == FSE_EmptyColorTag) - { - foundEmpty = true; - TEST_ExpectTrue(errors[i].count == 1); - } - if (errors[i].type == FSE_BadColor) - { - foundBadColor = true; - TEST_ExpectTrue(errors[i].cause.ToString() == "$cat"); - } - if (errors[i].type == FSE_BadGradientPoint) - { - foundBadPoint = true; - TEST_ExpectTrue(errors[i].cause.ToString() == "[%7]"); - } - if (errors[i].type == FSE_BadShortColorTag) - { - foundBadShortTag = true; - TEST_ExpectTrue(errors[i].cause.ToString() == "^z"); - } - } -}//^z, $cat, [%7] -/* // There was an unmatched closing figure bracket, e.g. - // "{$red Hey} you, there}!" - FSE_UnmatchedClosingBrackets, - // Color tag was empty, e.g. "Why not { just kill them}?" - FSE_EmptyColorTag, - // Color tag cannot be parsed as a color or color gradient, - // e.g. "Why not {just kill them}?" - FSE_BadColor, - // Gradient color tag contained bad point specified, e.g. - // "That is SO {$red~$orange(what?)~$red AMAZING}!!!" or - // "That is SO {$red~$orange(0.76~$red AMAZING}!!!" - FSE_BadGradientPoint, - FSE_BadShortColorTag */ protected static function Test_Simple() { Context("Testing parsing formatted strings with plain colors."); @@ -271,99 +70,102 @@ protected static function Test_Simple() protected static function SubTest_SimpleNone() { - local FormattedStringData data; + local MutableText result; + result = __().text.Empty(); Issue("Empty formatted strings are handled incorrectly."); - data = class'FormattedStringData'.static.FromText(P("")); - TEST_ExpectNotNone(data.GetResult()); - TEST_ExpectTrue(data.GetResult().IsEmpty()); + class'FormattingStringParser'.static.ParseFormatted(P(""), result); + TEST_ExpectNotNone(result); + TEST_ExpectTrue(result.IsEmpty()); Issue("Formatted strings with no content are handled incorrectly."); - data = class'FormattedStringData'.static.FromText(P("{$red }")); - TEST_ExpectNotNone(data.GetResult()); - TEST_ExpectTrue(data.GetResult().IsEmpty()); - data = class'FormattedStringData'.static - .FromText(P("{#ff03a5 {$blue }}^3{$lime }")); - TEST_ExpectNotNone(data.GetResult()); - TEST_ExpectTrue(data.GetResult().IsEmpty()); + class'FormattingStringParser'.static.ParseFormatted(P("{$red }"), result); + TEST_ExpectNotNone(result); + TEST_ExpectTrue(result.IsEmpty()); + class'FormattingStringParser'.static + .ParseFormatted(P("{#ff03a5 {$blue }}^3{$lime }"), result); + TEST_ExpectNotNone(result); + TEST_ExpectTrue(result.IsEmpty()); } protected static function SubTest_SimpleAlias() { - local MutableText example; - local FormattedStringData data; + local MutableText result, example; + result = __().text.Empty(); Issue("Formatted strings with aliases are handled incorrectly."); example = GetRGBText(); - data = class'FormattedStringData'.static - .FromText(P("This is {$red red}, {$lime green} and {$blue blue}!")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static.ParseFormatted( + P("This is {$red red}, {$lime green} and {$blue blue}!"), result); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); example = GetRGBText(true); - data = class'FormattedStringData'.static - .FromText(P("This is {$red red{$white , }{$lime green{$white and }}}" - $ "{$blue blue!}")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static.ParseFormatted( + P("This is {$red red{$white , }{$lime green{$white and }}}" + $ "{$blue blue!}"), + result.Clear()); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); } protected static function SubTest_SimpleRGB() { - local MutableText example; - local FormattedStringData data; + local MutableText result, example; + result = __().text.Empty(); Issue("Formatted strings with rgb definitions are handled incorrectly."); example = GetRGBText(); - data = class'FormattedStringData'.static - .FromText(P("This is {rgb(255,0,0) red}, {rgb(0,255,0) green} and" - @ "{rgb(0,0,255) blue}!")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static + .ParseFormatted(P("This is {rgb(255,0,0) red}, {rgb(0,255,0) green} and" + @ "{rgb(0,0,255) blue}!"), result); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); example = GetRGBText(true); - data = class'FormattedStringData'.static - .FromText(P("This is {rgb(255,0,0) red{rgb(255,255,255) , }" + class'FormattingStringParser'.static + .ParseFormatted(P("This is {rgb(255,0,0) red{rgb(255,255,255) , }" $ "{rgb(0,255,0) green{rgb(255,255,255) and }}}{rgb(0,0,255)" - $ " blue!}")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + $ " blue!}"), result.Clear()); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); } protected static function SubTest_SimpleHEX() { - local MutableText example; - local FormattedStringData data; + local MutableText result, example; + result = __().text.Empty(); Issue("Formatted strings with hex definitions are handled incorrectly."); example = GetRGBText(); - data = class'FormattedStringData'.static - .FromText(P("This is {#ff0000 red}, {#00ff00 green} and" - @ "{#0000ff blue}!")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static + .ParseFormatted(P("This is {#ff0000 red}, {#00ff00 green} and" + @ "{#0000ff blue}!"), result); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); example = GetRGBText(true); - data = class'FormattedStringData'.static - .FromText(P("This is {#ff0000 red{#ffffff , }" - $ "{#00ff00 green{#ffffff and }}}{#0000ff blue!}")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static.ParseFormatted( + P("This is {#ff0000 red{#ffffff , }{#00ff00 green{#ffffff and }}}" + $ "{#0000ff blue!}"), + result.Clear()); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); } protected static function SubTest_SimpleTag() { - local MutableText example; - local FormattedStringData data; + local MutableText result, example; + result = __().text.Empty(); Issue("Formatted strings with rag definitions are handled incorrectly."); example = GetRGBText(true); - data = class'FormattedStringData'.static - .FromText(P("This is ^rred^w, ^2green^w and ^4blue!")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static + .ParseFormatted(P("This is ^rred^w, ^2green^w and ^4blue!"), result); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); } protected static function SubTest_SimpleMix() { - local MutableText example; - local FormattedStringData data; + local MutableText result, example; + result = __().text.Empty(); Issue("Formatted strings with mixed definitions are handled incorrectly."); example = GetRGBText(); - data = class'FormattedStringData'.static - .FromText(P("This is {rgb(255,0,0) red}, {$lime green} and" - @ "{#af4378 ^bblue}!")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static + .ParseFormatted(P("This is {rgb(255,0,0) red}, {$lime green} and" + @ "{#af4378 ^bblue}!"), result); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); example = GetRGBText(true); - data = class'FormattedStringData'.static - .FromText(P("This is {$red red{rgb(255,255,255) , }" - $ "{#800c37d ^ggre^gen{#ffffff and }}}^bblue!")); - TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + class'FormattingStringParser'.static + .ParseFormatted(P("This is {$red red{rgb(255,255,255) , }" + $ "{#800c37d ^ggre^gen{#ffffff and }}}^bblue!"), result.Clear()); + TEST_ExpectTrue(example.Compare(result,, SFORM_SENSITIVE)); } protected static function Test_Gradient() @@ -378,16 +180,15 @@ protected static function Test_Gradient() protected static function SubTest_TestGradientTwoColors() { - local int i; - local Text result; - local FormattedStringData data; - local Color previousColor, currentColor; + local int i; + local Color previousColor, currentColor; + local MutableText result; + result = __().text.Empty(); Issue("Simple (two color) gradient block does not color intermediate" @ "characters correctly."); - data = class'FormattedStringData'.static - .FromText(P("{rgb(255,128,56)~rgb(0,255,56)" - @ "Simple shit to test out gradient}")); - result = data.GetResult(); + class'FormattingStringParser'.static + .ParseFormatted(P("{rgb(255,128,56):rgb(0,255,56)" + @ "Simple shit to test out gradient}"), result); previousColor = result.GetFormatting(0).color; TEST_ExpectTrue(result.GetFormatting(0).isColored); for (i = 1; i < result.GetLength(); i += 1) @@ -443,15 +244,14 @@ protected static function CheckRedIncrease(Text sample, int from, int to) protected static function SubTest_TestGradientThreeColors() { - local Text result; - local FormattedStringData data; - local Color borderColor; + local Color borderColor; + local MutableText result; + result = __().text.Empty(); Issue("Gradient block with three colors does not color intermediate" @ "characters correctly."); - data = class'FormattedStringData'.static - .FromText(P("{rgb(255,0,0)~#000000~$red" - @ "Simple shit to test out gradient!}")); - result = data.GetResult(); + class'FormattingStringParser'.static + .ParseFormatted(P("{rgb(255,0,0):#000000:$red" + @ "Simple shit to test out gradient!}"), result); CheckRedDecrease(result, 0, 16); CheckRedIncrease(result, 17, result.GetLength()); Issue("Gradient block with three colors does not color edge characters" @@ -466,14 +266,16 @@ protected static function SubTest_TestGradientThreeColors() protected static function SubTest_TestGradientFiveColors() { - local Text result; - local FormattedStringData data; - local Color borderColor; + local Color borderColor; + local MutableText result; + result = __().text.Empty(); Issue("Gradient block with five colors does not color intermediate" @ "characters correctly."); - data = class'FormattedStringData'.static - .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(200,0,0)~rgb(180,0,0)~rgb(210,0,0)~rgb(97,0,0) Go f yourself}!?!?!"));//27 SHIFT - result = data.GetResult(); + class'FormattingStringParser'.static.ParseFormatted( + P("Check this wacky shit out: {rgb(255,0,0):rgb(200,0,0):rgb(180,0,0)" + $ ":rgb(210,0,0):rgb(97,0,0) Go f yourself}!?!?!"), + result); + result = result; CheckRedDecrease(result, 0 + 27, 6 + 27); CheckRedIncrease(result, 7 + 27, 9 + 27); CheckRedDecrease(result, 9 + 27, 12 + 27); @@ -493,13 +295,14 @@ protected static function SubTest_TestGradientFiveColors() protected static function SubTest_TestGradientPoints() { - local Text result; - local FormattedStringData data; - local Color borderColor; + local Color borderColor; + local MutableText result; + result = __().text.Empty(); Issue("Gradient points are incorrectly handled."); - data = class'FormattedStringData'.static - .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(0,0,0)[25%]~rgb(123,0,0) Go f yourself}!?!?!")); - result = data.GetResult(); + class'FormattingStringParser'.static.ParseFormatted( + P("Check this wacky shit out: {rgb(255,0,0):rgb(0,0,0)[25%]:" + $ "rgb(123,0,0) Go f yourself}!?!?!"), + result); CheckRedDecrease(result, 0 + 27, 3 + 27); CheckRedIncrease(result, 3 + 27, 12 + 27); borderColor = result.GetFormatting(0 + 27).color; @@ -509,9 +312,10 @@ protected static function SubTest_TestGradientPoints() borderColor = result.GetFormatting(12 + 27).color; TEST_ExpectTrue(borderColor.r == 123); Issue("Gradient block does not color intermediate characters correctly."); - data = class'FormattedStringData'.static - .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(0,0,0)[0.75]~rgb(45,0,0) Go f yourself}!?!?!")); - result = data.GetResult(); + class'FormattingStringParser'.static.ParseFormatted( + P("Check this wacky shit out: {rgb(255,0,0):rgb(0,0,0)[0.75]:" + $ "rgb(45,0,0) Go f yourself}!?!?!"), + result.Clear()); CheckRedDecrease(result, 0 + 27, 9 + 27); CheckRedIncrease(result, 9 + 27, 12 + 27); borderColor = result.GetFormatting(0 + 27).color; @@ -524,13 +328,15 @@ protected static function SubTest_TestGradientPoints() protected static function SubTest_TestGradientPointsBad() { - local Text result; - local FormattedStringData data; - local Color borderColor; + local Color borderColor; + local MutableText result; + result = __().text.Empty(); Issue("Bad gradient points are incorrectly handled."); - data = class'FormattedStringData'.static - .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(128,0,0)[50%]~rgb(150,0,0)[0.3]~rgb(123,0,0) Go f yourself}!?!?!")); - result = data.GetResult(); + class'FormattingStringParser'.static.ParseFormatted( + P("Check this wacky shit out: {rgb(255,0,0):rgb(128,0,0)[50%]:" + $ "rgb(150,0,0)[0.3]:rgb(123,0,0) Go f yourself}!?!?!"), + result); + result = result; CheckRedDecrease(result, 0 + 27, 6 + 27); CheckRedIncrease(result, 6 + 27, 9 + 27); CheckRedDecrease(result, 9 + 27, 12 + 27); @@ -542,9 +348,10 @@ protected static function SubTest_TestGradientPointsBad() TEST_ExpectTrue(borderColor.r == 150); borderColor = result.GetFormatting(12 + 27).color; TEST_ExpectTrue(borderColor.r == 123); - data = class'FormattedStringData'.static - .FromText(P("Check this wacky shit out: {rgb(200,0,0)~rgb(255,0,0)[EDF]~rgb(0,0,0)[0.50]~rgb(45,0,0) Go f yourself}!?!?!")); - result = data.GetResult(); + class'FormattingStringParser'.static.ParseFormatted( + P("Check this wacky shit out: {rgb(200,0,0):rgb(255,0,0)[EDF]:" + $ "rgb(0,0,0)[0.50]:rgb(45,0,0) Go f yourself}!?!?!"), + result.Clear()); CheckRedIncrease(result, 0 + 27, 3 + 27); CheckRedDecrease(result, 3 + 27, 6 + 27); CheckRedIncrease(result, 6 + 27, 12 + 27); @@ -558,6 +365,195 @@ protected static function SubTest_TestGradientPointsBad() TEST_ExpectTrue(borderColor.r == 45); } +protected static function Test_Errors() +{ + Context("Testing error reporting for formatted strings."); + SubTest_ErrorUnmatchedClosingBrackets(); + SubTest_ErrorEmptyColorTag(); + SubTest_ErrorBadColor(); + SubTest_ErrorBadShortColorTag(); + SubTest_ErrorBadGradientPoint(); + SubTest_AllErrors(); +} + +protected static function SubTest_ErrorUnmatchedClosingBrackets() +{ + local array errors; + Issue("Unmatched closing brackets are not reported."); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing {$pink pink text}}!"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing regular text!}"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + errors = class'FormattingStringParser'.static + .ParseFormatted(P("This is {rgb(255,0,0) red{rgb(255,255,255) , }}}" + $ "{rgb(0,255,0) gr}een{rgb(255,255,255) and }}}}{rgb(0,0,255)" + $ " blue!}}}"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); + TEST_ExpectTrue(errors[0].count == 6); + TEST_ExpectNone(errors[0].cause); +} + +protected static function SubTest_ErrorEmptyColorTag() +{ + local array errors; + Issue("Empty color tags are not reported."); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing { pink text}!"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing {$red regu{ lar tex}t!}"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + errors = class'FormattingStringParser'.static + .ParseFormatted(P("This is { {rgb(255,255,255):$green , }" + $ "{#800c37 ^ggre^gen{ and }}}^bblue!"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); + TEST_ExpectTrue(errors[0].count == 2); + TEST_ExpectNone(errors[0].cause); +} + +protected static function SubTest_ErrorBadColor() +{ + local array errors; + Issue("Bad color is not reported."); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing {$cat pink text}!"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_BadColor); + TEST_ExpectTrue(errors[0].cause.ToString() == "$cat"); + TEST_ExpectTrue(errors[0].count == 0); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing {dog regular} {#wicked text!}"),, true); + TEST_ExpectTrue(errors.length == 2); + TEST_ExpectTrue(errors[0].type == FSE_BadColor); + TEST_ExpectTrue(errors[1].type == FSE_BadColor); + TEST_ExpectTrue(errors[0].cause.ToString() == "dog"); + TEST_ExpectTrue(errors[1].cause.ToString() == "#wicked"); + errors = class'FormattingStringParser'.static + .ParseFormatted(P("This is {goat red{rgb(255,255,255):lol:$green , }" + $ "{#800c37 ^ggre^gen{324sd and }}}^bblue!"),, true); + TEST_ExpectTrue(errors.length == 3); + TEST_ExpectTrue(errors[0].type == FSE_BadColor); + TEST_ExpectTrue(errors[1].type == FSE_BadColor); + TEST_ExpectTrue(errors[2].type == FSE_BadColor); + TEST_ExpectTrue(errors[0].cause.ToString() == "goat"); + TEST_ExpectTrue(errors[1].cause.ToString() == "lol"); + TEST_ExpectTrue(errors[2].cause.ToString() == "324sd"); +} + +protected static function SubTest_ErrorBadShortColorTag() +{ + local array errors; + Issue("Bad short color tag is not reported."); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("This is ^xred^w, ^ugreen^x and ^zblue!"),, true); + TEST_ExpectTrue(errors.length == 4); + TEST_ExpectTrue(errors[0].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[0].cause.ToString() == "^x"); + TEST_ExpectTrue(errors[0].count == 0); + TEST_ExpectTrue(errors[1].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[1].cause.ToString() == "^u"); + TEST_ExpectTrue(errors[1].count == 0); + TEST_ExpectTrue(errors[2].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[2].cause.ToString() == "^x"); + TEST_ExpectTrue(errors[2].count == 0); + TEST_ExpectTrue(errors[3].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[3].cause.ToString() == "^z"); + TEST_ExpectTrue(errors[3].count == 0); +} + +protected static function SubTest_ErrorBadGradientPoint() +{ + local array errors; + Issue("Bad gradient point is not reported."); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing {$pink[dog] pink text}!"),, true); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[0].cause.ToString() == "[dog]"); + TEST_ExpectTrue(errors[0].count == 0); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("Testing {45,2,241[bad] regular} {#ffaacd:rgb(2,3,4)45worse]" + @ "text!}"), + , + true); + TEST_ExpectTrue(errors.length == 2); + TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[1].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[0].cause.ToString() == "[bad]"); + TEST_ExpectTrue(errors[1].cause.ToString() == "45worse]"); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("This is {$red[45%%] red{rgb(255,255,255):45,3,128point:$green , }" + $ "{#800c37 ^ggre^gen{#43fa6b3c and }}}^bblue!"), + , + true); + TEST_ExpectTrue(errors.length == 3); + TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[1].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[2].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[0].cause.ToString() == "[45%%]"); + TEST_ExpectTrue(errors[1].cause.ToString() == "point"); + TEST_ExpectTrue(errors[2].cause.ToString() == "3c"); +} + +protected static function SubTest_AllErrors() +{ + local int i; + local bool foundUnmatched, foundEmpty, foundBadColor; + local bool foundBadPoint, foundBadShortTag; + local array errors; + Issue("If formatted string contains several errors, not all of them are" + @ "properly detected."); + errors = class'FormattingStringParser'.static.ParseFormatted( + P("This} is {$cat:$green[%7] red{$white , }{ green^z and }}" + $ "{$blue blue!}}"), + , + true); + for (i = 0; i < errors.length; i += 1) + { + if (errors[i].type == FSE_UnmatchedClosingBrackets) + { + foundUnmatched = true; + TEST_ExpectTrue(errors[i].count == 2); + } + if (errors[i].type == FSE_EmptyColorTag) + { + foundEmpty = true; + TEST_ExpectTrue(errors[i].count == 1); + } + if (errors[i].type == FSE_BadColor) + { + foundBadColor = true; + TEST_ExpectTrue(errors[i].cause.ToString() == "$cat"); + } + if (errors[i].type == FSE_BadGradientPoint) + { + foundBadPoint = true; + TEST_ExpectTrue(errors[i].cause.ToString() == "[%7]"); + } + if (errors[i].type == FSE_BadShortColorTag) + { + foundBadShortTag = true; + TEST_ExpectTrue(errors[i].cause.ToString() == "^z"); + } + } +} + defaultproperties { caseName = "FormattedStrings"