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"