Browse Source

Refactored new code for parsing formatted strings

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
acc31767e5
  1. 404
      sources/Text/FormattedStrings/FormattedStringData.uc
  2. 296
      sources/Text/FormattedStrings/FormattingCommandList.uc
  3. 316
      sources/Text/FormattedStrings/FormattingCommandsSequence.uc
  4. 96
      sources/Text/FormattedStrings/FormattingErrorsReport.uc
  5. 548
      sources/Text/FormattedStrings/FormattingStringParser.uc
  6. 20
      sources/Text/MutableText.uc
  7. 586
      sources/Text/Tests/TEST_FormattedStrings.uc

404
sources/Text/FormattedStrings/FormattedStringData.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
class FormattedStringData extends AcediaObject
dependson(Text)
dependson(FormattingErrors)
dependson(FormattingCommandList);
struct FormattingInfo
{
var bool colored;
var Color plainColor;
var bool gradient;
var array<Color> gradientColors;
var array<float> 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<FormattingInfo> 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<Text.Character> 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<float> 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<MutableText> specifiedColors;
local Text.Character tildeCharacter;
local array<Color> gradientColors;
local array<float> 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<float> NormalizePoints(array<float> 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
{
}

296
sources/Text/FormattedStrings/FormattingCommandList.uc

@ -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 <https://www.gnu.org/licenses/>.
*/
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 ("{<color_tag> ") 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 ("{<color_tag> ").
// 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<Text.Character> 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<FormattingCommand> 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<int> pushCommandIndicesStack;
// Store contents for the next command here, because appending array in
// the struct is expensive
var private array<Text.Character> 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 "{<formatting_info>"
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
{
}

316
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 <https://www.gnu.org/licenses/>.
*/
class FormattingCommandsSequence extends AcediaObject
dependson(Text);
enum FormattingCommandType
{
// Push more new formatting onto the stack. Corresponds to "{<color_tag> ".
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 "^<color_char>".
FST_StackSwap
};
/**
* Represents formatting command + contents, alongside some additional
* meta information, necessary for `FormattingStringParser`.
*/
struct FormattingCommand
{
var FormattingCommandType type;
var array<Text.Character> 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 ("{<color_tag> "):
// 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 - "<color_tag>" from "{<color_tag> ".
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<FormattingCommand> commandSequence;
// Store contents for the next command here, because constantly appending array
// inside the struct (`FormattingCommand` here) is expensive.
var private array<Text.Character> 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 "{<color_tag> ". 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<int> 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 "{<formatting_info> "
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
{
}

96
sources/Text/FormattedStrings/FormattingErrors.uc → 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 <https://www.gnu.org/licenses/>.
*/
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<Text> badColorTagErrorHints;
var private array<Text> badGradientTagErrorHints;
var private array<Text> 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<FormattedDataError> GetErrors()
public final function array<FormattedStringError> GetErrors()
{
local int i;
local FormattedDataError newError;
local array<FormattedDataError> errors;
// We overwrite old `cause` in `newError` with new one each time we
local int i;
local FormattedStringError newError;
local array<FormattedStringError> 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<FormattedDataError> 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;
}

548
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 <https://www.gnu.org/licenses/>.
*/
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 "{<color_tag> ...}" as a set
* of two operations: turn on a certain formatting ("{<color_tag>") 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 "^<color_character>" 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
* ("<color_1>:<color_2>:<color_3>[<point>]:<color_4>"). 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<Color> gradientColors;
// Points (from 0 to 1) at which each `gradientColors` with the same index
// should be at its 100%
var array<float> 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<FormattingInfo> 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<FormattingErrorsReport.FormattedStringError>
ParseFormatted(
Text source,
optional MutableText target,
optional bool doReportErrors)
{
local FormattingErrorsReport newErrorsReport;
local FormattingStringParser newFormattingParser;
local array<FormattingErrorsReport.FormattedStringError> 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<Text.Character> 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<float> 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<MutableText> specifiedColors;
local array<Color> gradientColors;
local array<float> 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<float> NormalizePoints(array<float> 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) = "%"
}

20
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);

586
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<FormattingErrors.FormattedDataError> 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<FormattingErrors.FormattedDataError> 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<FormattingErrors.FormattedDataError> 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<FormattingErrors.FormattedDataError> 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<FormattingErrors.FormattedDataError> 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<FormattingErrors.FormattedDataError> 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<FormattingErrorsReport.FormattedStringError> 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<FormattingErrorsReport.FormattedStringError> 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<FormattingErrorsReport.FormattedStringError> 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<FormattingErrorsReport.FormattedStringError> 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<FormattingErrorsReport.FormattedStringError> 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<FormattingErrorsReport.FormattedStringError> 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"

Loading…
Cancel
Save