diff --git a/sources/Color/ColorAPI.uc b/sources/Color/ColorAPI.uc
index d34ad8b..73127b1 100644
--- a/sources/Color/ColorAPI.uc
+++ b/sources/Color/ColorAPI.uc
@@ -3,7 +3,7 @@
* It has a wide range of pre-defined colors and some functions that
* allow to quickly assemble color from rgb(a) values, parse it from
* a `Text`/string or load it from an alias.
- * Copyright 2020 Anton Tarasenko
+ * Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -954,11 +954,13 @@ public final function bool ParseWith(Parser parser, out Color resultingColor)
local MutableText colorAlias;
local Parser colorParser;
local Parser.ParserState initialParserState;
- if (parser == none) return false;
+ if (parser == none) {
+ return false;
+ }
resultingColor.a = 0xff;
colorParser = parser;
initialParserState = parser.GetCurrentState();
- if (parser.Match(T(TDOLLAR)).MUntil(colorAlias,, true).Ok())
+ if (parser.Match(T(TDOLLAR)).MName(colorAlias).Ok())
{
colorContent = _.alias.ResolveColor(colorAlias);
colorParser = _.text.Parse(colorContent);
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 25d3c81..d934ef7 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -1,6 +1,6 @@
/**
* Manifest is meant to describe contents of the Acedia's package.
- * Copyright 2020 - 2021 Anton Tarasenko
+ * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -41,18 +41,19 @@ defaultproperties
testCases(10) = class'TEST_Parser'
testCases(11) = class'TEST_JSON'
testCases(12) = class'TEST_TextCache'
- testCases(13) = class'TEST_User'
- testCases(14) = class'TEST_Memory'
- testCases(15) = class'TEST_DynamicArray'
- testCases(16) = class'TEST_AssociativeArray'
- testCases(17) = class'TEST_CollectionsMixed'
- testCases(18) = class'TEST_Iterator'
- testCases(19) = class'TEST_Command'
- testCases(20) = class'TEST_CommandDataBuilder'
- testCases(21) = class'TEST_LogMessage'
- testCases(22) = class'TEST_DatabaseCommon'
- testCases(23) = class'TEST_LocalDatabase'
- testCases(24) = class'TEST_AcediaConfig'
- testCases(25) = class'TEST_UTF8EncoderDecoder'
- testCases(26) = class'TEST_AvariceStreamReader'
+ testCases(13) = class'TEST_FormattedStrings'
+ testCases(14) = class'TEST_User'
+ testCases(15) = class'TEST_Memory'
+ testCases(16) = class'TEST_DynamicArray'
+ testCases(17) = class'TEST_AssociativeArray'
+ testCases(18) = class'TEST_CollectionsMixed'
+ testCases(19) = class'TEST_Iterator'
+ testCases(20) = class'TEST_Command'
+ testCases(21) = class'TEST_CommandDataBuilder'
+ testCases(22) = class'TEST_LogMessage'
+ testCases(23) = class'TEST_DatabaseCommon'
+ testCases(24) = class'TEST_LocalDatabase'
+ testCases(25) = class'TEST_AcediaConfig'
+ testCases(26) = class'TEST_UTF8EncoderDecoder'
+ testCases(27) = class'TEST_AvariceStreamReader'
}
\ No newline at end of file
diff --git a/sources/Text/FormattedStrings/FormattedStringData.uc b/sources/Text/FormattedStrings/FormattedStringData.uc
new file mode 100644
index 0000000..c00470c
--- /dev/null
+++ b/sources/Text/FormattedStrings/FormattedStringData.uc
@@ -0,0 +1,404 @@
+/**
+ * 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
new file mode 100644
index 0000000..17a779f
--- /dev/null
+++ b/sources/Text/FormattedStrings/FormattingCommandList.uc
@@ -0,0 +1,296 @@
+/**
+ * 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/FormattingErrors.uc b/sources/Text/FormattedStrings/FormattingErrors.uc
new file mode 100644
index 0000000..1e26256
--- /dev/null
+++ b/sources/Text/FormattedStrings/FormattingErrors.uc
@@ -0,0 +1,179 @@
+/**
+ * Simple aggregator object for errors that may arise during parsing of
+ * formatted string.
+ * 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 FormattingErrors extends AcediaObject;
+
+/**
+ * Errors that can occur during parsing of the formatted string.
+ */
+enum FormattedDataErrorType
+{
+ // 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
+};
+
+// `FSE_UnmatchedClosingBrackets` and `FSE_EmptyColorTag` errors never have any
+// `Text` hint associated with them, so simply store how many times they were
+// 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.
+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
+{
+ // Type of the error
+ var FormattedDataErrorType type;
+ // How many times had this error happened?
+ // Can be specified for `FSE_UnmatchedClosingBrackets` and
+ // `FSE_EmptyColorTag` error types. Never negative.
+ 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;
+};
+
+protected function Finalizer()
+{
+ unmatchedClosingBracketsErrorCount = 0;
+ emptyColorTagErrorCount = 0;
+ _.memory.FreeMany(badColorTagErrorHints);
+ _.memory.FreeMany(badShortColorTagErrorHints);
+ _.memory.FreeMany(badGradientTagErrorHints);
+ badColorTagErrorHints.length = 0;
+ badShortColorTagErrorHints.length = 0;
+ badGradientTagErrorHints.length = 0;
+}
+
+/**
+ * Adds new error to the caller `FormattingErrors` 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.
+ */
+public final function Report(FormattedDataErrorType type, optional Text cause)
+{
+ switch (type)
+ {
+ case FSE_UnmatchedClosingBrackets:
+ unmatchedClosingBracketsErrorCount += 1;
+ break;
+ case FSE_EmptyColorTag:
+ emptyColorTagErrorCount += 1;
+ break;
+ case FSE_BadColor:
+ if (cause != none) {
+ badColorTagErrorHints[badColorTagErrorHints.length] = cause.Copy();
+ }
+ break;
+ case FSE_BadShortColorTag:
+ if (cause != none)
+ {
+ badShortColorTagErrorHints[badShortColorTagErrorHints.length] =
+ cause.Copy();
+ }
+ break;
+ case FSE_BadGradientPoint:
+ if (cause != none)
+ {
+ badGradientTagErrorHints[badGradientTagErrorHints.length] =
+ cause.Copy();
+ }
+ break;
+ }
+}
+
+/**
+ * Returns array of errors collected so far.
+ *
+ * @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.
+ */
+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
+ // add new error, so it should be fine to not set it to `none` after
+ // "moving it" into `errors`.
+ newError.type = FSE_BadColor;
+ for (i = 0; i < badColorTagErrorHints.length; i += 1)
+ {
+ newError.cause = badColorTagErrorHints[i].Copy();
+ errors[errors.length] = newError;
+ }
+ newError.type = FSE_BadShortColorTag;
+ for (i = 0; i < badShortColorTagErrorHints.length; i += 1)
+ {
+ newError.cause = badShortColorTagErrorHints[i].Copy();
+ errors[errors.length] = newError;
+ }
+ newError.type = FSE_BadGradientPoint;
+ for (i = 0; i < badGradientTagErrorHints.length; i += 1)
+ {
+ 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;
+}
+
+defaultproperties
+{
+}
\ No newline at end of file
diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc
index 821a2d6..dca84d3 100644
--- a/sources/Text/MutableText.uc
+++ b/sources/Text/MutableText.uc
@@ -1,6 +1,6 @@
/**
* Mutable version of Acedia's `Text`
- * Copyright 2020 - 2021 Anton Tarasenko
+ * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -19,59 +19,7 @@
*/
class MutableText extends Text;
-var private int CODEPOINT_NEWLINE, CODEPOINT_ACCENT;
-
-enum FormattedStackCommandType
-{
- // Push more data onto formatted stack
- FST_StackPush,
- // Pop data from formatted stack
- FST_StackPop,
- // Swap the top value on the formatting stack
- FST_StackSwap
-};
-
-// Formatted `string` is separated into several (possibly nested) parts,
-// each with its own formatting. These can be easily handled with a formatting
-// stack:
-// * Each time a new section opens ("{ contents;
- // Formatting tag for the next block
- // (only used for `FST_StackPush` command type)
- var MutableText tag;
- // Formatting character for the "^"-type tag
- // (only used for `FST_StackSwap` command type)
- var Character charTag;
-};
-// Appending formatted `string` into the `MutableText` first requires its
-// transformation into series of `FormattedStackCommand` and then their
-// execution to assemble the `MutableText`.
-// First element of `stackCommands` is special and is used solely as
-// a container for unformatted data. It should not be used to execute
-// formatting stack commands.
-// This variable contains intermediary data.
-var array stackCommands;
-// Formatted `string` can have an arbitrary level of folded format definitions,
-// this array is used as a stack to keep track of opened formatting blocks
-// when appending formatted `string`.
-var array formattingStack;
+var private int CODEPOINT_NEWLINE;
/**
* Clears all current data from the caller `MutableText` instance.
@@ -84,6 +32,55 @@ public final function MutableText Clear()
return self;
}
+/**
+ * Appends a new character to the caller `MutableText`, while discarding its
+ * own formatting.
+ *
+ * @param newCharacter Character to add to the caller `MutableText`.
+ * Only valid characters will be added. Its formatting will be discarded.
+ * @param characterFormatting You can use this parameter to specify formatting
+ * `newCharacter` should have in the caller `MutableText` instead of
+ * its own.
+ * @return Caller `MutableText` to allow for method chaining.
+ */
+public final function MutableText AppendRawCharacter(
+ Text.Character newCharacter,
+ optional Formatting characterFormatting)
+{
+ if (!_.text.IsValidCharacter(newCharacter)) {
+ return self;
+ }
+ SetFormatting(characterFormatting);
+ return MutableText(AppendCodePoint(newCharacter.codePoint));
+}
+
+/**
+ * Appends all characters from the given array, in order, but discarding their
+ * own formatting.
+ *
+ * This method should be faster than `AppendManyCharacters()` or several calls
+ * of `AppendRawCharacter()`, since it does not need to check whether
+ * formatting is changed from character to character.
+ *
+ * @param newCharacters Characters to be added to the caller `MutableText`.
+ * Only valid characters will be added. Their formatting will be discarded.
+ * @param characterFormatting You can use this parameter to specify formatting
+ * `newCharacters` should have in the caller `MutableText` instead of
+ * their own.
+ * @return Caller `MutableText` to allow for method chaining.
+ */
+public final function MutableText AppendManyRawCharacters(
+ array newCharacters,
+ optional Formatting charactersFormatting)
+{
+ local int i;
+ SetFormatting(charactersFormatting);
+ for (i = 0; i < newCharacters.length; i += 1) {
+ AppendCodePoint(newCharacters[i].codePoint);
+ }
+ return self;
+}
+
/**
* Appends a new character to the caller `MutableText`.
*
@@ -100,6 +97,23 @@ public final function MutableText AppendCharacter(Text.Character newCharacter)
return MutableText(AppendCodePoint(newCharacter.codePoint));
}
+/**
+ * Appends all characters from the given array, in order.
+ *
+ * @param newCharacters Characters to be added to the caller `MutableText`.
+ * Only valid characters will be added.
+ * @return Caller `MutableText` to allow for method chaining.
+ */
+public final function MutableText AppendManyCharacters(
+ array newCharacters)
+{
+ local int i;
+ for (i = 0; i < newCharacters.length; i += 1) {
+ AppendCharacter(newCharacters[i]);
+ }
+ return self;
+}
+
/**
* Adds new line character to the end of the caller `MutableText`.
*
@@ -271,10 +285,14 @@ public final function MutableText AppendFormatted(
Text source,
optional Formatting defaultFormatting)
{
- local Parser parser;
- parser = _.text.Parse(source);
- AppendFormattedParser(parser, defaultFormatting);
- parser.FreeSelf();
+ // 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);
return self;
}
@@ -291,174 +309,14 @@ public final function MutableText AppendFormattedString(
string source,
optional Formatting defaultFormatting)
{
- local Parser parser;
- parser = _.text.ParseString(source);
- AppendFormattedParser(parser, defaultFormatting);
- parser.FreeSelf();
+ // TODO: is this the best way?
+ local Text sourceAsText;
+ sourceAsText = _.text.FromString(source);
+ AppendFormatted(sourceAsText);
+ _.memory.Free(sourceAsText);
return self;
}
-/**
- * Appends contents of the formatted `string` to the caller `MutableText`.
- *
- * @param source Formatted `string` to be appended to
- * the caller `MutableText`.
- * @param defaultFormatting Formatting to be used for `source`'s characters
- * that have no color information defined.
- * @return Caller `MutableText` to allow for method chaining.
- */
-private final function MutableText AppendFormattedParser(
- Parser sourceParser,
- optional Formatting defaultFormatting)
-{
- local int i;
- local Parser tagParser;
- BuildFormattingStackCommands(sourceParser);
- if (stackCommands.length <= 0) {
- return self;
- }
- SetupFormattingStack(defaultFormatting);
- tagParser = Parser(_.memory.Allocate(class'Parser'));
- SetFormatting(defaultFormatting);
- // First element of color stack is special and has no color information;
- // see `BuildFormattingStackCommands()` for details.
- AppendManyCodePoints(stackCommands[0].contents);
- for (i = 1; i < stackCommands.length; i += 1)
- {
- if (stackCommands[i].type == FST_StackPush)
- {
- tagParser.Initialize(stackCommands[i].tag);
- SetFormatting(PushIntoFormattingStack(tagParser));
- }
- else if (stackCommands[i].type == FST_StackPop) {
- SetFormatting(PopFormattingStack());
- }
- else if (stackCommands[i].type == FST_StackSwap) {
- SetFormatting(SwapFormattingStack(stackCommands[i].charTag));
- }
- AppendManyCodePoints(stackCommands[i].contents);
- _.memory.Free(stackCommands[i].tag);
- }
- stackCommands.length = 0;
- _.memory.Free(tagParser);
- return self;
-}
-
-// Function that parses formatted `string` into array of
-// `FormattedStackCommand`s.
-// Returned array is guaranteed to always have at least one element.
-// First element in array always corresponds to part of the input string
-// (`source`) without any formatting defined, even if it's empty.
-// This is to avoid having fourth command type, only usable at the beginning.
-private final function BuildFormattingStackCommands(Parser parser)
-{
- local Character nextCharacter;
- local FormattedStackCommand nextCommand;
- stackCommands.length = 0;
- while (!parser.HasFinished())
- {
- parser.MCharacter(nextCharacter);
- // New command by "{"
- if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT))
- {
- stackCommands[stackCommands.length] = nextCommand;
- nextCommand = CreateStackCommand(FST_StackPush);
- parser.MUntil(nextCommand.tag,, true)
- .MCharacter(nextCommand.charTag); // Simply to skip a char
- continue;
- }
- // New command by "}"
- if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT))
- {
- stackCommands[stackCommands.length] = nextCommand;
- nextCommand = CreateStackCommand(FST_StackPop);
- continue;
- }
- // New command by "^"
- if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT))
- {
- stackCommands[stackCommands.length] = nextCommand;
- nextCommand = CreateStackCommand(FST_StackSwap);
- parser.MCharacter(nextCommand.charTag);
- if (!parser.Ok()) {
- break;
- }
- continue;
- }
- // Escaped sequence
- if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) {
- parser.MCharacter(nextCharacter);
- }
- if (!parser.Ok()) {
- break;
- }
- nextCommand.contents[nextCommand.contents.length] =
- nextCharacter.codePoint;
- }
- // Only put in empty command if there is nothing else.
- if (nextCommand.contents.length > 0 || stackCommands.length == 0) {
- stackCommands[stackCommands.length] = nextCommand;
- }
-}
-
-// 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)
-{
- formattingStack.length = 0;
- formattingStack[0] = defaultFormatting;
-}
-
-private final function Formatting PushIntoFormattingStack(
- Parser formattingDefinitionParser)
-{
- local Formatting newFormatting;
- if (_.color.ParseWith(formattingDefinitionParser, newFormatting.color)) {
- newFormatting.isColored = true;
- }
- formattingStack[formattingStack.length] = newFormatting;
- return newFormatting;
-}
-
-private final function Formatting SwapFormattingStack(Character tagCharacter)
-{
- local Formatting updatedFormatting;
- if (formattingStack.length <= 0) {
- return updatedFormatting;
- }
- updatedFormatting = formattingStack[formattingStack.length - 1];
- if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.color)) {
- updatedFormatting.isColored = true;
- }
- formattingStack[formattingStack.length - 1] = updatedFormatting;
- return updatedFormatting;
-}
-
-private final function Formatting PopFormattingStack()
-{
- local Formatting result;
- formattingStack.length = Max(1, formattingStack.length - 1);
- if (formattingStack.length > 0) {
- result = formattingStack[formattingStack.length - 1];
- }
- return result;
-}
-
-// Helper method for a quick creation of a new `FormattedStackCommand`
-private final function FormattedStackCommand CreateStackCommand(
- FormattedStackCommandType stackCommandType)
-{
- local FormattedStackCommand newCommand;
- newCommand.type = stackCommandType;
- return newCommand;
-}
-
/**
* Unlike `Text`, `MutableText` can change it's content and therefore it's
* hash code cannot depend on it. So we restore `AcediaObject`'s behavior and
@@ -679,6 +537,5 @@ public final function MutableText Simplify(optional bool fixInnerSpacings)
defaultproperties
{
- CODEPOINT_NEWLINE = 10
- CODEPOINT_ACCENT = 94
+ CODEPOINT_NEWLINE = 10
}
\ No newline at end of file
diff --git a/sources/Text/Parser.uc b/sources/Text/Parser.uc
index 04a0945..f02d12c 100644
--- a/sources/Text/Parser.uc
+++ b/sources/Text/Parser.uc
@@ -1,7 +1,7 @@
/**
* Implements a simple `Parser` with built-in functions to parse simple
* UnrealScript's types and support for saving / restoring parser states.
- * Copyright 2021 Anton Tarasenko
+ * Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -34,7 +34,7 @@ var public int CODEPOINT_ULARGE;
var private Text content;
// Incremented each time `Parser` is reinitialized with new `content`.
// Can be used to make `Parser` object completely independent from
-// it's past after each re-initialization.
+// its past after each re-initialization.
// This helps to avoid needless reallocations.
var private int version;
@@ -126,7 +126,7 @@ public final function Parser InitializeS(string source)
* Checks if `Parser` is in a failed state.
*
* Parser enters a failed state whenever any parsing call returns without
- * completing it's job. `Parser` in a failed state will automatically fail
+ * completing its job. `Parser` in a failed state will automatically fail
* any further parsing attempts until it gets reset via `R()` call.
*
* @return Returns 'false' if `Parser()` is in a failed state and
@@ -141,7 +141,7 @@ public final function bool Ok()
* Returns copy of the current state of this parser.
*
* As long as caller `Parser` was not reinitialized, returned `ParserState`
- * structure can be used to revert this `Parser` to it's current condition
+ * structure can be used to revert this `Parser` to its current condition
* by a `RestoreState()` call.
*
* @see `RestoreState()`
@@ -156,7 +156,7 @@ public final function ParserState GetCurrentState()
* Returns copy of (currently) last confirmed state of this parser.
*
* As long as caller `Parser` was not reinitialized, returned `ParserState`
- * structure can be used to revert this `Parser` to it's current confirmed
+ * structure can be used to revert this `Parser` to its current confirmed
* state by a `RestoreState()` call.
*
* @see `RestoreState()`, `Confirm()`, `R()`
@@ -253,7 +253,7 @@ public final function bool Confirm()
/**
* Resets `Parser` to a last state recorded as confirmed by a last successful
* `Confirm()` function call. If there weren't any such call -
- * reverts `Parser` to it's state right after initialization.
+ * reverts `Parser` to its state right after initialization.
*
* Always resets failed state of a `Parser`. Cannot fail.
*
@@ -302,7 +302,7 @@ protected final function Parser ShiftPointer(optional int shift)
* and does not fit `Parser`'s contents, returns invalid character.
* `GetCodePoint()` with default (`0`) parameter can also return
* invalid character if caller `Parser` was not initialized,
- * it's contents are empty or it has already consumed all input.
+ * its contents are empty or it has already consumed all input.
*/
protected final function Text.Character GetCharacter(optional int shift)
{
@@ -361,7 +361,7 @@ public final function int GetRemainingLength()
}
/**
- * Checks if caller `Parser` has already parsed all of it's content.
+ * Checks if caller `Parser` has already parsed all of its content.
* Uninitialized `Parser` has no content and, therefore, parsed it all.
*
* Should return `true` iff `GetRemainingLength() == 0`.
@@ -668,6 +668,72 @@ public final function Parser MEscapedSequence(
return self;
}
+/**
+ * Attempts to parse a "name": a string literal that:
+ * 1. Contains only digits and latin characters;
+ * 2. Starts with a latin character.
+ * These restrictions help to avoid possible issues that arise from having
+ * different code pages and are used. For example, only "names" are considered
+ * to be valid aliases.
+ *
+ * @param result If parsing is successful, this `MutableText` will contain
+ * the contents of the matched "name", if parsing has failed, its value
+ * is undefined. Any passed contents are simply discarded.
+ * If passed `MutableText` equals to `none`, new instance will be
+ * automatically allocated. This will be done regardless of whether
+ * parsing fails.
+ * @return Returns the caller `Parser`, to allow for function chaining.
+ */
+public final function Parser MName(out MutableText result)
+{
+ local TextAPI api;
+ local Text.Character nextCharacter;
+ ResetResultText(result);
+ if (!Ok()) return self;
+ if (GetRemainingLength() <= 0) return Fail();
+ api = _.text;
+ nextCharacter = GetCharacter();
+ if (!api.IsAlpha(nextCharacter)) return Fail();
+
+ result.AppendCharacter(nextCharacter);
+ ShiftPointer();
+ nextCharacter = GetCharacter();
+ while (api.IsAlpha(nextCharacter) || api.IsDigit(nextCharacter))
+ {
+ result.AppendCharacter(nextCharacter);
+ ShiftPointer();
+ nextCharacter = GetCharacter();
+ }
+ return self;
+}
+
+/**
+ * Attempts to parse a "name": a string literal that:
+ * 1. Contains only digits and latin characters;
+ * 2. Starts with a latin character.
+ * These restrictions help to avoid possible issues that arise from having
+ * different code pages and are used. For example, only "names" are considered
+ * to be valid aliases.
+ *
+ * @param result If parsing is successful, this `string` will contain the
+ * contents of the matched "name" with resolved escaped sequences;
+ * if parsing has failed, its value is undefined.
+ * Any passed contents are simply discarded.
+ * @return Returns the caller `Parser`, to allow for function chaining.
+ */
+public final function Parser MNameS(out string result)
+{
+ local MutableText wrapper;
+ if (!Ok()) return self;
+
+ wrapper = _.text.Empty();
+ if (MName(wrapper).Ok()) {
+ result = wrapper.ToString();
+ }
+ wrapper.FreeSelf();
+ return self;
+}
+
/**
* Attempts to parse a string literal: a string enclosed in either of
* the following quotation marks: ", ', `.
@@ -678,7 +744,7 @@ public final function Parser MEscapedSequence(
*
* @param result If parsing is successful, this `MutableText` will contain
* the contents of string literal with resolved escaped sequences;
- * if parsing has failed, it's value is undefined.
+ * if parsing has failed, its value is undefined.
* Any passed contents are simply discarded.
* If passed `MutableText` equals to `none`, new instance will be
* automatically allocated. This will be done regardless of whether
@@ -736,7 +802,7 @@ public final function Parser MStringLiteral(out MutableText result)
*
* @param result If parsing is successful, this `string` will contain the
* contents of string literal with resolved escaped sequences;
- * if parsing has failed, it's value is undefined.
+ * if parsing has failed, its value is undefined.
* Any passed contents are simply discarded.
* @return Returns the caller `Parser`, to allow for function chaining.
*/
diff --git a/sources/Text/Tests/TEST_FormattedStrings.uc b/sources/Text/Tests/TEST_FormattedStrings.uc
new file mode 100644
index 0000000..caeeee3
--- /dev/null
+++ b/sources/Text/Tests/TEST_FormattedStrings.uc
@@ -0,0 +1,565 @@
+/**
+ * Set of tests for functionality of parsing formatted strings.
+ * Copyright 2022 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class TEST_FormattedStrings extends TestCase
+ abstract;
+
+protected static function MutableText GetRGBText(
+ optional bool noFormattingReset)
+{
+ local int wordIndex;
+ local MutableText result;
+ result = __().text.FromStringM("This is red, green and blue!");
+ wordIndex = result.IndexOf(P("red"));
+ result.ChangeFormatting(__().text.FormattingFromColor(__().color.red),
+ wordIndex, 3);
+ wordIndex = result.IndexOf(P("green"));
+ result.ChangeFormatting(__().text.FormattingFromColor(__().color.lime),
+ wordIndex, 5);
+ wordIndex = result.IndexOf(P("blue"));
+ result.ChangeFormatting(__().text.FormattingFromColor(__().color.blue),
+ wordIndex, 4);
+ // also color ", " and " and " parts white, and "!" part blue
+ if (noFormattingReset)
+ {
+ result.ChangeFormatting(__().text.FormattingFromColor(__().color.blue),
+ wordIndex, -1);
+ wordIndex = result.IndexOf(P(", "));
+ result.ChangeFormatting(__().text.FormattingFromColor(__().color.white),
+ wordIndex, 2);
+ wordIndex = result.IndexOf(P(" and "));
+ result.ChangeFormatting(__().text.FormattingFromColor(__().color.white),
+ wordIndex, 5);
+ }
+ return result;
+}
+
+protected static function TESTS()
+{
+ Test_Simple();
+ Test_Gradient();
+ 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.");
+ SubTest_SimpleNone();
+ SubTest_SimpleAlias();
+ SubTest_SimpleRGB();
+ SubTest_SimpleHEX();
+ SubTest_SimpleTag();
+ SubTest_SimpleMix();
+}
+
+protected static function SubTest_SimpleNone()
+{
+ local FormattedStringData data;
+ Issue("Empty formatted strings are handled incorrectly.");
+ data = class'FormattedStringData'.static.FromText(P(""));
+ TEST_ExpectNotNone(data.GetResult());
+ TEST_ExpectTrue(data.GetResult().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());
+}
+
+protected static function SubTest_SimpleAlias()
+{
+ local MutableText example;
+ local FormattedStringData data;
+ 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));
+ 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));
+}
+
+protected static function SubTest_SimpleRGB()
+{
+ local MutableText example;
+ local FormattedStringData data;
+ 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));
+ example = GetRGBText(true);
+ data = class'FormattedStringData'.static
+ .FromText(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));
+}
+
+protected static function SubTest_SimpleHEX()
+{
+ local MutableText example;
+ local FormattedStringData data;
+ 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));
+ 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));
+}
+
+protected static function SubTest_SimpleTag()
+{
+ local MutableText example;
+ local FormattedStringData data;
+ 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));
+}
+
+protected static function SubTest_SimpleMix()
+{
+ local MutableText example;
+ local FormattedStringData data;
+ 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));
+ 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));
+}
+
+protected static function Test_Gradient()
+{
+ Context("Testing parsing formatted strings with gradient.");
+ SubTest_TestGradientTwoColors();
+ SubTest_TestGradientThreeColors();
+ SubTest_TestGradientFiveColors();
+ SubTest_TestGradientPoints();
+ SubTest_TestGradientPointsBad();
+}
+
+protected static function SubTest_TestGradientTwoColors()
+{
+ local int i;
+ local Text result;
+ local FormattedStringData data;
+ local Color previousColor, currentColor;
+ 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();
+ previousColor = result.GetFormatting(0).color;
+ TEST_ExpectTrue(result.GetFormatting(0).isColored);
+ for (i = 1; i < result.GetLength(); i += 1)
+ {
+ TEST_ExpectTrue(result.GetFormatting(i).isColored);
+ currentColor = result.GetFormatting(i).color;
+ TEST_ExpectTrue(previousColor.r > currentColor.r);
+ TEST_ExpectTrue(previousColor.g < currentColor.g);
+ TEST_ExpectTrue(previousColor.b == currentColor.b);
+ previousColor = currentColor;
+ }
+ Issue("Gradient (two color) block does not color edge characters"
+ @ "correctly.");
+ previousColor = result.GetFormatting(0).color;
+ currentColor = result.GetFormatting(result.GetLength() - 1).color;
+ TEST_ExpectTrue(previousColor.r == 255);
+ TEST_ExpectTrue(previousColor.g == 128);
+ TEST_ExpectTrue(previousColor.b == 56);
+ TEST_ExpectTrue(currentColor.r == 0);
+ TEST_ExpectTrue(currentColor.g == 255);
+ TEST_ExpectTrue(currentColor.b == 56);
+}
+
+protected static function CheckRedDecrease(Text sample, int from, int to)
+{
+ local int i;
+ local Color previousColor, currentColor;
+ previousColor = sample.GetFormatting(from).color;
+ TEST_ExpectTrue(sample.GetFormatting(from).isColored);
+ for (i = from + 1; i < to; i += 1)
+ {
+ TEST_ExpectTrue(sample.GetFormatting(i).isColored);
+ currentColor = sample.GetFormatting(i).color;
+ TEST_ExpectTrue(previousColor.r > currentColor.r);
+ previousColor = currentColor;
+ }
+}
+
+protected static function CheckRedIncrease(Text sample, int from, int to)
+{
+ local int i;
+ local Color previousColor, currentColor;
+ previousColor = sample.GetFormatting(from).color;
+ TEST_ExpectTrue(sample.GetFormatting(from).isColored);
+ for (i = from + 1; i < to; i += 1)
+ {
+ TEST_ExpectTrue(sample.GetFormatting(i).isColored);
+ currentColor = sample.GetFormatting(i).color;
+ TEST_ExpectTrue(previousColor.r < currentColor.r);
+ previousColor = currentColor;
+ }
+}
+
+protected static function SubTest_TestGradientThreeColors()
+{
+ local Text result;
+ local FormattedStringData data;
+ local Color borderColor;
+ 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();
+ CheckRedDecrease(result, 0, 16);
+ CheckRedIncrease(result, 17, result.GetLength());
+ Issue("Gradient block with three colors does not color edge characters"
+ @ "correctly.");
+ borderColor = result.GetFormatting(0).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(result.GetLength() - 1).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(16).color;
+ TEST_ExpectTrue(borderColor.r == 0);
+}
+
+protected static function SubTest_TestGradientFiveColors()
+{
+ local Text result;
+ local FormattedStringData data;
+ local Color borderColor;
+ 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();
+ CheckRedDecrease(result, 0 + 27, 6 + 27);
+ CheckRedIncrease(result, 7 + 27, 9 + 27);
+ CheckRedDecrease(result, 9 + 27, 12 + 27);
+ Issue("Gradient block with five colors does not color edge characters"
+ @ "correctly.");
+ borderColor = result.GetFormatting(0 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(3 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 200);
+ borderColor = result.GetFormatting(6 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 180);
+ borderColor = result.GetFormatting(9 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 210);
+ borderColor = result.GetFormatting(12 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 97);
+}
+
+protected static function SubTest_TestGradientPoints()
+{
+ local Text result;
+ local FormattedStringData data;
+ local Color borderColor;
+ 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();
+ CheckRedDecrease(result, 0 + 27, 3 + 27);
+ CheckRedIncrease(result, 3 + 27, 12 + 27);
+ borderColor = result.GetFormatting(0 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(3 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 0);
+ 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();
+ CheckRedDecrease(result, 0 + 27, 9 + 27);
+ CheckRedIncrease(result, 9 + 27, 12 + 27);
+ borderColor = result.GetFormatting(0 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(9 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 0);
+ borderColor = result.GetFormatting(12 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 45);
+}
+
+protected static function SubTest_TestGradientPointsBad()
+{
+ local Text result;
+ local FormattedStringData data;
+ local Color borderColor;
+ 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();
+ CheckRedDecrease(result, 0 + 27, 6 + 27);
+ CheckRedIncrease(result, 6 + 27, 9 + 27);
+ CheckRedDecrease(result, 9 + 27, 12 + 27);
+ borderColor = result.GetFormatting(0 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(6 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 128);
+ borderColor = result.GetFormatting(9 + 27).color;
+ 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();
+ CheckRedIncrease(result, 0 + 27, 3 + 27);
+ CheckRedDecrease(result, 3 + 27, 6 + 27);
+ CheckRedIncrease(result, 6 + 27, 12 + 27);
+ borderColor = result.GetFormatting(0 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 200);
+ borderColor = result.GetFormatting(3 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 255);
+ borderColor = result.GetFormatting(6 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 0);
+ borderColor = result.GetFormatting(12 + 27).color;
+ TEST_ExpectTrue(borderColor.r == 45);
+}
+
+defaultproperties
+{
+ caseName = "FormattedStrings"
+ caseGroup = "Text"
+}
\ No newline at end of file
diff --git a/sources/Text/Tests/TEST_Parser.uc b/sources/Text/Tests/TEST_Parser.uc
index 716a350..2abd84f 100644
Binary files a/sources/Text/Tests/TEST_Parser.uc and b/sources/Text/Tests/TEST_Parser.uc differ
diff --git a/sources/Text/Tests/TEST_Text.uc b/sources/Text/Tests/TEST_Text.uc
index ecaf395..d6e2f25 100644
Binary files a/sources/Text/Tests/TEST_Text.uc and b/sources/Text/Tests/TEST_Text.uc differ
diff --git a/sources/Text/Text.uc b/sources/Text/Text.uc
index bf63409..422545c 100644
--- a/sources/Text/Text.uc
+++ b/sources/Text/Text.uc
@@ -11,7 +11,7 @@
* a representation (e.g. faster hash calculation was implemented).
* 3. Provides an additional layer of abstraction that can potentially
* allow for an improved Unicode support.
- * Copyright 2020 - 2021 Anton Tarasenko
+ * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -60,8 +60,8 @@ struct Formatting
// Represents one character, together with it's formatting
struct Character
{
- var int codePoint;
- var Formatting formatting;
+ var int codePoint;
+ var Formatting formatting;
};
// Actual content of the `Text` is stored as a sequence of Unicode code points.
@@ -1299,9 +1299,13 @@ public final function string ToFormattedString(
* single-element array containing copy of this `Text`.
*
* @param separator Character that separates different parts of this `Text`.
+ * @param skipEmpty Set this to `true` to filter out empty `MutableText`s
+ * from the output.
* @return Array of `MutableText`s that contain separated substrings.
*/
-public final function array SplitByCharacter(Character separator)
+public final function array SplitByCharacter(
+ Character separator,
+ optional bool skipEmpty)
{
local int i, length;
local Character nextCharacter;
@@ -1315,7 +1319,12 @@ public final function array SplitByCharacter(Character separator)
nextCharacter = GetCharacter(i);
if (_.text.AreEqual(separator, nextCharacter))
{
- result[result.length] = nextText;
+ if (!skipEmpty || !nextText.IsEmpty()) {
+ result[result.length] = nextText;
+ }
+ else {
+ _.memory.Free(nextText);
+ }
nextText = _.text.Empty();
}
else {
@@ -1323,7 +1332,9 @@ public final function array SplitByCharacter(Character separator)
}
i += 1;
}
- result[result.length] = nextText;
+ if (!skipEmpty || !nextText.IsEmpty()) {
+ result[result.length] = nextText;
+ }
return result;
}
diff --git a/sources/Text/TextAPI.uc b/sources/Text/TextAPI.uc
index 5f44383..ea01acf 100644
--- a/sources/Text/TextAPI.uc
+++ b/sources/Text/TextAPI.uc
@@ -1,7 +1,7 @@
/**
* API that provides functions for working with characters and for creating
* `Text` and `Parser` instances.
- * Copyright 2020 - 2021 Anton Tarasenko
+ * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -144,10 +144,31 @@ public final function bool IsDigit(Text.Character character)
return false;
}
+/**
+ * Checks if given character corresponds to a latin alphabet.
+ *
+ * @param codePoint Unicode code point to check for belonging in
+ * the alphabet.
+ * @return `true` if given Unicode code point belongs to a latin alphabet,
+ * `false` otherwise.
+ */
+public final function bool IsAlpha(Text.Character character)
+{
+ // Capital Latin letters
+ if (character.codePoint >= 65 && character.codePoint <= 90) {
+ return true;
+ }
+ // Small Latin letters
+ if (character.codePoint >= 97 && character.codePoint <= 122) {
+ return true;
+ }
+ return false;
+}
+
/**
* Checks if given character is an ASCII character.
*
- * @param character Character to check for being a digit.
+ * @param character Character to check for being from ASCII.
* @return `true` if given character is a digit, `false` otherwise.
*/
public final function bool IsASCII(Text.Character character)
diff --git a/sources/Types/Tests/TEST_Base.uc b/sources/Types/Tests/TEST_Base.uc
index 298731b..ce8e552 100644
--- a/sources/Types/Tests/TEST_Base.uc
+++ b/sources/Types/Tests/TEST_Base.uc
@@ -1,7 +1,7 @@
/**
* Set of tests for some of the build-in methods for
* Acedia's objects/actors.
- * Copyright 2020 Anton Tarasenko
+ * Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -68,7 +68,8 @@ protected static function Test_QuickText()
protected static function Test_Constants()
{
- local Text old1, old2;
+ local Text old1, old2;
+ local int old1Lifetime, old2Lifetime;
Context("Testing `T()` for returning `Text` generated from"
@ "`stringConstants`.");
Issue("Expected `Text`s are not correctly generated.");
@@ -91,12 +92,16 @@ protected static function Test_Constants()
@ "a new one.");
old1 = T(0);
old2 = T(1);
+ old1Lifetime = old1.GetLifeVersion();
+ old2Lifetime = old2.GetLifeVersion();
old1.FreeSelf();
old2.FreeSelf();
- TEST_ExpectTrue( old2 != T(1)
- || old2.GetLifeVersion() != T(1).GetLifeVersion());
- TEST_ExpectTrue( old1 != T(0)
- || old1.GetLifeVersion() != T(0).GetLifeVersion());
+ TEST_ExpectTrue(old2 != T(1) || old2Lifetime != T(1).GetLifeVersion());
+ TEST_ExpectTrue(old1 != T(0) || old1Lifetime != T(0).GetLifeVersion());
+ TEST_ExpectTrue(T(0).IsAllocated());
+ TEST_ExpectTrue(T(1).IsAllocated());
+ TEST_ExpectTrue(T(0).ToString() == "boolean");
+ TEST_ExpectTrue(T(1).ToString() == "byte");
}
defaultproperties