Browse Source

Add color gradient blocks to formatted string

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
b9f96af43e
  1. 8
      sources/Color/ColorAPI.uc
  2. 31
      sources/Manifest.uc
  3. 404
      sources/Text/FormattedStrings/FormattedStringData.uc
  4. 296
      sources/Text/FormattedStrings/FormattingCommandList.uc
  5. 179
      sources/Text/FormattedStrings/FormattingErrors.uc
  6. 305
      sources/Text/MutableText.uc
  7. 86
      sources/Text/Parser.uc
  8. 565
      sources/Text/Tests/TEST_FormattedStrings.uc
  9. BIN
      sources/Text/Tests/TEST_Parser.uc
  10. BIN
      sources/Text/Tests/TEST_Text.uc
  11. 15
      sources/Text/Text.uc
  12. 25
      sources/Text/TextAPI.uc
  13. 15
      sources/Types/Tests/TEST_Base.uc

8
sources/Color/ColorAPI.uc

@ -3,7 +3,7 @@
* It has a wide range of pre-defined colors and some functions that * 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 * allow to quickly assemble color from rgb(a) values, parse it from
* a `Text`/string or load it from an alias. * a `Text`/string or load it from an alias.
* Copyright 2020 Anton Tarasenko * Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -954,11 +954,13 @@ public final function bool ParseWith(Parser parser, out Color resultingColor)
local MutableText colorAlias; local MutableText colorAlias;
local Parser colorParser; local Parser colorParser;
local Parser.ParserState initialParserState; local Parser.ParserState initialParserState;
if (parser == none) return false; if (parser == none) {
return false;
}
resultingColor.a = 0xff; resultingColor.a = 0xff;
colorParser = parser; colorParser = parser;
initialParserState = parser.GetCurrentState(); initialParserState = parser.GetCurrentState();
if (parser.Match(T(TDOLLAR)).MUntil(colorAlias,, true).Ok()) if (parser.Match(T(TDOLLAR)).MName(colorAlias).Ok())
{ {
colorContent = _.alias.ResolveColor(colorAlias); colorContent = _.alias.ResolveColor(colorAlias);
colorParser = _.text.Parse(colorContent); colorParser = _.text.Parse(colorContent);

31
sources/Manifest.uc

@ -1,6 +1,6 @@
/** /**
* Manifest is meant to describe contents of the Acedia's package. * 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. * This file is part of Acedia.
* *
@ -41,18 +41,19 @@ defaultproperties
testCases(10) = class'TEST_Parser' testCases(10) = class'TEST_Parser'
testCases(11) = class'TEST_JSON' testCases(11) = class'TEST_JSON'
testCases(12) = class'TEST_TextCache' testCases(12) = class'TEST_TextCache'
testCases(13) = class'TEST_User' testCases(13) = class'TEST_FormattedStrings'
testCases(14) = class'TEST_Memory' testCases(14) = class'TEST_User'
testCases(15) = class'TEST_DynamicArray' testCases(15) = class'TEST_Memory'
testCases(16) = class'TEST_AssociativeArray' testCases(16) = class'TEST_DynamicArray'
testCases(17) = class'TEST_CollectionsMixed' testCases(17) = class'TEST_AssociativeArray'
testCases(18) = class'TEST_Iterator' testCases(18) = class'TEST_CollectionsMixed'
testCases(19) = class'TEST_Command' testCases(19) = class'TEST_Iterator'
testCases(20) = class'TEST_CommandDataBuilder' testCases(20) = class'TEST_Command'
testCases(21) = class'TEST_LogMessage' testCases(21) = class'TEST_CommandDataBuilder'
testCases(22) = class'TEST_DatabaseCommon' testCases(22) = class'TEST_LogMessage'
testCases(23) = class'TEST_LocalDatabase' testCases(23) = class'TEST_DatabaseCommon'
testCases(24) = class'TEST_AcediaConfig' testCases(24) = class'TEST_LocalDatabase'
testCases(25) = class'TEST_UTF8EncoderDecoder' testCases(25) = class'TEST_AcediaConfig'
testCases(26) = class'TEST_AvariceStreamReader' testCases(26) = class'TEST_UTF8EncoderDecoder'
testCases(27) = class'TEST_AvariceStreamReader'
} }

404
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 <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

@ -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 <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
{
}

179
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 <https://www.gnu.org/licenses/>.
*/
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<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
{
// 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<FormattedDataError> GetErrors()
{
local int i;
local FormattedDataError newError;
local array<FormattedDataError> 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
{
}

305
sources/Text/MutableText.uc

@ -1,6 +1,6 @@
/** /**
* Mutable version of Acedia's `Text` * Mutable version of Acedia's `Text`
* Copyright 2020 - 2021 Anton Tarasenko * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -19,59 +19,7 @@
*/ */
class MutableText extends Text; class MutableText extends Text;
var private int CODEPOINT_NEWLINE, CODEPOINT_ACCENT; var private int CODEPOINT_NEWLINE;
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 ("{<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.
// Logic that parses formatted `string` works is broken into two steps:
// 1. Read formatted `string` detecting "{<color_tag ", "}" and "^"
// sequences and build a series of stack commands (along with data that
// should be appended after them);
// 2. use these commands to construct `MutableText`.
struct FormattedStackCommand
{
// Did this block start by opening or closing formatted part?
// Ignored for the very first block without any formatting.
var FormattedStackCommandType type;
// Full text inside the block, without any formatting
var array<int> 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<FormattedStackCommand> 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<Formatting> formattingStack;
/** /**
* Clears all current data from the caller `MutableText` instance. * Clears all current data from the caller `MutableText` instance.
@ -84,6 +32,55 @@ public final function MutableText Clear()
return self; 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<Text.Character> 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`. * 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)); 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<Text.Character> 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`. * Adds new line character to the end of the caller `MutableText`.
* *
@ -271,10 +285,14 @@ public final function MutableText AppendFormatted(
Text source, Text source,
optional Formatting defaultFormatting) optional Formatting defaultFormatting)
{ {
local Parser parser; // TODO: is this the best way?
parser = _.text.Parse(source); local Text appendedPart;
AppendFormattedParser(parser, defaultFormatting); local FormattedStringData data;
parser.FreeSelf(); data = class'FormattedStringData'.static.FromText(source);
appendedPart = data.GetResult();
Append(appendedPart);
_.memory.Free(appendedPart);
_.memory.Free(data);
return self; return self;
} }
@ -291,173 +309,13 @@ public final function MutableText AppendFormattedString(
string source, string source,
optional Formatting defaultFormatting) optional Formatting defaultFormatting)
{ {
local Parser parser; // TODO: is this the best way?
parser = _.text.ParseString(source); local Text sourceAsText;
AppendFormattedParser(parser, defaultFormatting); sourceAsText = _.text.FromString(source);
parser.FreeSelf(); AppendFormatted(sourceAsText);
return self; _.memory.Free(sourceAsText);
}
/**
* 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; 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 "{<color>"
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 * Unlike `Text`, `MutableText` can change it's content and therefore it's
@ -680,5 +538,4 @@ public final function MutableText Simplify(optional bool fixInnerSpacings)
defaultproperties defaultproperties
{ {
CODEPOINT_NEWLINE = 10 CODEPOINT_NEWLINE = 10
CODEPOINT_ACCENT = 94
} }

86
sources/Text/Parser.uc

@ -1,7 +1,7 @@
/** /**
* Implements a simple `Parser` with built-in functions to parse simple * Implements a simple `Parser` with built-in functions to parse simple
* UnrealScript's types and support for saving / restoring parser states. * 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. * This file is part of Acedia.
* *
@ -34,7 +34,7 @@ var public int CODEPOINT_ULARGE;
var private Text content; var private Text content;
// Incremented each time `Parser` is reinitialized with new `content`. // Incremented each time `Parser` is reinitialized with new `content`.
// Can be used to make `Parser` object completely independent from // 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. // This helps to avoid needless reallocations.
var private int version; var private int version;
@ -126,7 +126,7 @@ public final function Parser InitializeS(string source)
* Checks if `Parser` is in a failed state. * Checks if `Parser` is in a failed state.
* *
* Parser enters a failed state whenever any parsing call returns without * 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. * any further parsing attempts until it gets reset via `R()` call.
* *
* @return Returns 'false' if `Parser()` is in a failed state and * @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. * Returns copy of the current state of this parser.
* *
* As long as caller `Parser` was not reinitialized, returned `ParserState` * 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. * by a `RestoreState()` call.
* *
* @see `RestoreState()` * @see `RestoreState()`
@ -156,7 +156,7 @@ public final function ParserState GetCurrentState()
* Returns copy of (currently) last confirmed state of this parser. * Returns copy of (currently) last confirmed state of this parser.
* *
* As long as caller `Parser` was not reinitialized, returned `ParserState` * 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. * state by a `RestoreState()` call.
* *
* @see `RestoreState()`, `Confirm()`, `R()` * @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 * Resets `Parser` to a last state recorded as confirmed by a last successful
* `Confirm()` function call. If there weren't any such call - * `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. * 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. * and does not fit `Parser`'s contents, returns invalid character.
* `GetCodePoint()` with default (`0`) parameter can also return * `GetCodePoint()` with default (`0`) parameter can also return
* invalid character if caller `Parser` was not initialized, * 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) 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. * Uninitialized `Parser` has no content and, therefore, parsed it all.
* *
* Should return `true` iff `GetRemainingLength() == 0`. * Should return `true` iff `GetRemainingLength() == 0`.
@ -668,6 +668,72 @@ public final function Parser MEscapedSequence(
return self; 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 * Attempts to parse a string literal: a string enclosed in either of
* the following quotation marks: ", ', `. * the following quotation marks: ", ', `.
@ -678,7 +744,7 @@ public final function Parser MEscapedSequence(
* *
* @param result If parsing is successful, this `MutableText` will contain * @param result If parsing is successful, this `MutableText` will contain
* the contents of string literal with resolved escaped sequences; * 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. * Any passed contents are simply discarded.
* If passed `MutableText` equals to `none`, new instance will be * If passed `MutableText` equals to `none`, new instance will be
* automatically allocated. This will be done regardless of whether * 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 * @param result If parsing is successful, this `string` will contain the
* contents of string literal with resolved escaped sequences; * 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. * Any passed contents are simply discarded.
* @return Returns the caller `Parser`, to allow for function chaining. * @return Returns the caller `Parser`, to allow for function chaining.
*/ */

565
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 <https://www.gnu.org/licenses/>.
*/
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<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.");
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"
}

BIN
sources/Text/Tests/TEST_Parser.uc

Binary file not shown.

BIN
sources/Text/Tests/TEST_Text.uc

Binary file not shown.

15
sources/Text/Text.uc

@ -11,7 +11,7 @@
* a representation (e.g. faster hash calculation was implemented). * a representation (e.g. faster hash calculation was implemented).
* 3. Provides an additional layer of abstraction that can potentially * 3. Provides an additional layer of abstraction that can potentially
* allow for an improved Unicode support. * allow for an improved Unicode support.
* Copyright 2020 - 2021 Anton Tarasenko * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -1299,9 +1299,13 @@ public final function string ToFormattedString(
* single-element array containing copy of this `Text`. * single-element array containing copy of this `Text`.
* *
* @param separator Character that separates different parts 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. * @return Array of `MutableText`s that contain separated substrings.
*/ */
public final function array<MutableText> SplitByCharacter(Character separator) public final function array<MutableText> SplitByCharacter(
Character separator,
optional bool skipEmpty)
{ {
local int i, length; local int i, length;
local Character nextCharacter; local Character nextCharacter;
@ -1315,7 +1319,12 @@ public final function array<MutableText> SplitByCharacter(Character separator)
nextCharacter = GetCharacter(i); nextCharacter = GetCharacter(i);
if (_.text.AreEqual(separator, nextCharacter)) if (_.text.AreEqual(separator, nextCharacter))
{ {
if (!skipEmpty || !nextText.IsEmpty()) {
result[result.length] = nextText; result[result.length] = nextText;
}
else {
_.memory.Free(nextText);
}
nextText = _.text.Empty(); nextText = _.text.Empty();
} }
else { else {
@ -1323,7 +1332,9 @@ public final function array<MutableText> SplitByCharacter(Character separator)
} }
i += 1; i += 1;
} }
if (!skipEmpty || !nextText.IsEmpty()) {
result[result.length] = nextText; result[result.length] = nextText;
}
return result; return result;
} }

25
sources/Text/TextAPI.uc

@ -1,7 +1,7 @@
/** /**
* API that provides functions for working with characters and for creating * API that provides functions for working with characters and for creating
* `Text` and `Parser` instances. * `Text` and `Parser` instances.
* Copyright 2020 - 2021 Anton Tarasenko * Copyright 2020 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -144,10 +144,31 @@ public final function bool IsDigit(Text.Character character)
return false; 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. * 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. * @return `true` if given character is a digit, `false` otherwise.
*/ */
public final function bool IsASCII(Text.Character character) public final function bool IsASCII(Text.Character character)

15
sources/Types/Tests/TEST_Base.uc

@ -1,7 +1,7 @@
/** /**
* Set of tests for some of the build-in methods for * Set of tests for some of the build-in methods for
* Acedia's objects/actors. * Acedia's objects/actors.
* Copyright 2020 Anton Tarasenko * Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -69,6 +69,7 @@ protected static function Test_QuickText()
protected static function Test_Constants() 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" Context("Testing `T()` for returning `Text` generated from"
@ "`stringConstants`."); @ "`stringConstants`.");
Issue("Expected `Text`s are not correctly generated."); Issue("Expected `Text`s are not correctly generated.");
@ -91,12 +92,16 @@ protected static function Test_Constants()
@ "a new one."); @ "a new one.");
old1 = T(0); old1 = T(0);
old2 = T(1); old2 = T(1);
old1Lifetime = old1.GetLifeVersion();
old2Lifetime = old2.GetLifeVersion();
old1.FreeSelf(); old1.FreeSelf();
old2.FreeSelf(); old2.FreeSelf();
TEST_ExpectTrue( old2 != T(1) TEST_ExpectTrue(old2 != T(1) || old2Lifetime != T(1).GetLifeVersion());
|| old2.GetLifeVersion() != T(1).GetLifeVersion()); TEST_ExpectTrue(old1 != T(0) || old1Lifetime != T(0).GetLifeVersion());
TEST_ExpectTrue( old1 != T(0) TEST_ExpectTrue(T(0).IsAllocated());
|| old1.GetLifeVersion() != T(0).GetLifeVersion()); TEST_ExpectTrue(T(1).IsAllocated());
TEST_ExpectTrue(T(0).ToString() == "boolean");
TEST_ExpectTrue(T(1).ToString() == "byte");
} }
defaultproperties defaultproperties

Loading…
Cancel
Save