From b9f96af43e30a2633ab8b1c2dce550a9eb3dfce8 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Thu, 16 Jun 2022 04:03:52 +0700 Subject: [PATCH] Add color gradient blocks to formatted string --- sources/Color/ColorAPI.uc | 8 +- sources/Manifest.uc | 31 +- .../FormattedStrings/FormattedStringData.uc | 404 +++++++++++++ .../FormattedStrings/FormattingCommandList.uc | 296 +++++++++ .../Text/FormattedStrings/FormattingErrors.uc | 179 ++++++ sources/Text/MutableText.uc | 307 +++------- sources/Text/Parser.uc | 86 ++- sources/Text/Tests/TEST_FormattedStrings.uc | 565 ++++++++++++++++++ sources/Text/Tests/TEST_Parser.uc | Bin 112906 -> 117800 bytes sources/Text/Tests/TEST_Text.uc | Bin 116638 -> 122570 bytes sources/Text/Text.uc | 23 +- sources/Text/TextAPI.uc | 25 +- sources/Types/Tests/TEST_Base.uc | 17 +- 13 files changed, 1674 insertions(+), 267 deletions(-) create mode 100644 sources/Text/FormattedStrings/FormattedStringData.uc create mode 100644 sources/Text/FormattedStrings/FormattingCommandList.uc create mode 100644 sources/Text/FormattedStrings/FormattingErrors.uc create mode 100644 sources/Text/Tests/TEST_FormattedStrings.uc diff --git a/sources/Color/ColorAPI.uc b/sources/Color/ColorAPI.uc index d34ad8b..73127b1 100644 --- a/sources/Color/ColorAPI.uc +++ b/sources/Color/ColorAPI.uc @@ -3,7 +3,7 @@ * It has a wide range of pre-defined colors and some functions that * allow to quickly assemble color from rgb(a) values, parse it from * a `Text`/string or load it from an alias. - * Copyright 2020 Anton Tarasenko + * Copyright 2020-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -954,11 +954,13 @@ public final function bool ParseWith(Parser parser, out Color resultingColor) local MutableText colorAlias; local Parser colorParser; local Parser.ParserState initialParserState; - if (parser == none) return false; + if (parser == none) { + return false; + } resultingColor.a = 0xff; colorParser = parser; initialParserState = parser.GetCurrentState(); - if (parser.Match(T(TDOLLAR)).MUntil(colorAlias,, true).Ok()) + if (parser.Match(T(TDOLLAR)).MName(colorAlias).Ok()) { colorContent = _.alias.ResolveColor(colorAlias); colorParser = _.text.Parse(colorContent); diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 25d3c81..d934ef7 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -1,6 +1,6 @@ /** * Manifest is meant to describe contents of the Acedia's package. - * Copyright 2020 - 2021 Anton Tarasenko + * Copyright 2020 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -41,18 +41,19 @@ defaultproperties testCases(10) = class'TEST_Parser' testCases(11) = class'TEST_JSON' testCases(12) = class'TEST_TextCache' - testCases(13) = class'TEST_User' - testCases(14) = class'TEST_Memory' - testCases(15) = class'TEST_DynamicArray' - testCases(16) = class'TEST_AssociativeArray' - testCases(17) = class'TEST_CollectionsMixed' - testCases(18) = class'TEST_Iterator' - testCases(19) = class'TEST_Command' - testCases(20) = class'TEST_CommandDataBuilder' - testCases(21) = class'TEST_LogMessage' - testCases(22) = class'TEST_DatabaseCommon' - testCases(23) = class'TEST_LocalDatabase' - testCases(24) = class'TEST_AcediaConfig' - testCases(25) = class'TEST_UTF8EncoderDecoder' - testCases(26) = class'TEST_AvariceStreamReader' + testCases(13) = class'TEST_FormattedStrings' + testCases(14) = class'TEST_User' + testCases(15) = class'TEST_Memory' + testCases(16) = class'TEST_DynamicArray' + testCases(17) = class'TEST_AssociativeArray' + testCases(18) = class'TEST_CollectionsMixed' + testCases(19) = class'TEST_Iterator' + testCases(20) = class'TEST_Command' + testCases(21) = class'TEST_CommandDataBuilder' + testCases(22) = class'TEST_LogMessage' + testCases(23) = class'TEST_DatabaseCommon' + testCases(24) = class'TEST_LocalDatabase' + testCases(25) = class'TEST_AcediaConfig' + testCases(26) = class'TEST_UTF8EncoderDecoder' + testCases(27) = class'TEST_AvariceStreamReader' } \ No newline at end of file diff --git a/sources/Text/FormattedStrings/FormattedStringData.uc b/sources/Text/FormattedStrings/FormattedStringData.uc new file mode 100644 index 0000000..c00470c --- /dev/null +++ b/sources/Text/FormattedStrings/FormattedStringData.uc @@ -0,0 +1,404 @@ +/** + * Object that is created from *formatted string* (or `Text`) and stores + * information about formatting used in said string. Was introduced instead of + * a simple method in `MutableText` to: + * 1. Allow for reporting errors caused by badly specified colors; + * 2. Allow for a more complicated case of specifying a color gradient + * range. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class FormattedStringData extends AcediaObject + dependson(Text) + dependson(FormattingErrors) + dependson(FormattingCommandList); + +struct FormattingInfo +{ + var bool colored; + var Color plainColor; + var bool gradient; + var array gradientColors; + var array gradientPoints; + var int gradientStart; + var float gradientLength; +}; +// Formatted `string` can have an arbitrary level of folded format definitions, +// this array is used as a stack to keep track of opened formatting blocks +// when appending formatted `string`. +var array formattingStack; +// Keep top element copied into a separate variable for quicker access. +// Must maintain invariant: if `formattingStack.length > 0` +// then `formattingStack[formattingStack.length - 1] == formattingStackHead`. +var FormattingInfo formattingStackHead; + +var private FormattingCommandList commands; +var private MutableText result; +var private FormattingErrors errors; + +protected function Finalizer() +{ + formattingStack.length = 0; + _.memory.Free(commands); + _.memory.Free(errors); + _.memory.Free(result); + commands = none; + errors = none; + result = none; +} + +public static final function FormattedStringData FromText( + Text source, + optional bool doReportErrors) +{ + local FormattedStringData newData; + if (source == none) { + return none; + } + newData = + FormattedStringData(__().memory.Allocate(class'FormattedStringData')); + if (doReportErrors) + { + newData.errors = + FormattingErrors(__().memory.Allocate(class'FormattingErrors')); + } + newData.commands = class'FormattingCommandList'.static + .FromText(source, newData.errors); + newData.result = __().text.Empty(); + newData.BuildSelf(); + __().memory.Free(newData.commands); + newData.commands = none; + return newData; +} + +public final function Text GetResult() +{ + return result.Copy(); +} + +public final function MutableText GetResultM() +{ + return result.MutableCopy(); +} + +public final function FormattingErrors BorrowErrors() +{ + return errors; +} + +private final function BuildSelf() +{ + local int i, j, nextCharacterIndex; + local Text.Formatting defaultFormatting; + local array nextContents; + local FormattingCommandList.FormattingCommand nextCommand; + SetupFormattingStack(defaultFormatting); + // First element of color stack is special and has no color information; + // see `BuildFormattingStackCommands()` for details. + nextCommand = commands.GetCommand(0); + nextContents = nextCommand.contents; + result.AppendManyRawCharacters(nextContents); + nextCharacterIndex = nextContents.length; + _.memory.Free(nextCommand.tag); + for (i = 1; i < commands.GetAmount(); i += 1) + { + nextCommand = commands.GetCommand(i); + if (nextCommand.type == FST_StackPush) { + PushIntoFormattingStack(nextCommand); + } + else if (nextCommand.type == FST_StackPop) { + PopFormattingStack(); + } + else if (nextCommand.type == FST_StackSwap) { + SwapFormattingStack(nextCommand.charTag); + } + nextContents = nextCommand.contents; + if (IsCurrentFormattingGradient()) + { + for (j = 0; j < nextContents.length; j += 1) + { + result.AppendRawCharacter(nextContents[j], GetFormattingFor(nextCharacterIndex)); + nextCharacterIndex += 1; + } + } + else + { + result.AppendManyRawCharacters(nextContents, GetFormattingFor(nextCharacterIndex)); + nextCharacterIndex += nextContents.length; + } + _.memory.Free(nextCommand.tag); + } +} + +// Following four functions are to maintain a "color stack" that will +// remember unclosed colors (new colors are obtained from formatting commands +// sequence) defined in formatted string, in order. +// Stack array always contains one element, defined by +// the `SetupFormattingStack()` call. It corresponds to the default formatting +// that will be used when we pop all the other elements. +// It is necessary to deal with possible folded formatting definitions in +// formatted strings. +private final function SetupFormattingStack(Text.Formatting defaultFormatting) +{ + local FormattingInfo defaultFormattingInfo; + defaultFormattingInfo.colored = defaultFormatting.isColored; + defaultFormattingInfo.plainColor = defaultFormatting.color; + if (formattingStack.length > 0) { + formattingStack.length = 0; + } + formattingStack[0] = defaultFormattingInfo; + formattingStackHead = defaultFormattingInfo; +} + +private final function bool IsCurrentFormattingGradient() +{ + if (formattingStack.length <= 0) { + return false; + } + return formattingStackHead.gradient; +} + +private final function Text.Formatting GetFormattingFor(int index) +{ + local Text.Formatting emptyFormatting; + if (formattingStack.length <= 0) return emptyFormatting; + if (!formattingStackHead.colored) return emptyFormatting; + + return _.text.FormattingFromColor(GetColorFor(index)); +} +//FormattedStringData Package.FormattedStringData (Function AcediaCore.FormattedStringData.GetColorFor:00FC) Accessed array 'gradientColors' out of bounds (2/2) +private final function Color GetColorFor(int index) +{ + local int i; + local float indexPosition, leftPosition, rightPosition; + local array points; + local Color leftColor, rightColor, resultColor; + if (formattingStack.length <= 0) { + return resultColor; + } + if (!formattingStackHead.gradient) { + return formattingStackHead.plainColor; + } + indexPosition = float(index - formattingStackHead.gradientStart) / + formattingStackHead.gradientLength; + points = formattingStackHead.gradientPoints; + for (i = 1; i < points.length; i += 1) + { + if (points[i - 1] <= indexPosition && indexPosition <= points[i]) + { + leftPosition = points[i - 1]; + rightPosition = points[i]; + leftColor = formattingStackHead.gradientColors[i - 1]; + rightColor = formattingStackHead.gradientColors[i]; + break; + } + } + indexPosition = + (indexPosition - leftPosition) / (rightPosition - leftPosition); + resultColor.R = Lerp(indexPosition, leftColor.R, rightColor.R); + resultColor.G = Lerp(indexPosition, leftColor.G, rightColor.G); + resultColor.B = Lerp(indexPosition, leftColor.B, rightColor.B); + resultColor.A = Lerp(indexPosition, leftColor.A, rightColor.A); + return resultColor; +} + +private final function PushIntoFormattingStack( + FormattingCommandList.FormattingCommand formattingCommand) +{ + formattingStackHead = ParseFormattingInfo(formattingCommand.tag); + formattingStackHead.gradientStart = formattingCommand.openIndex; + formattingStackHead.gradientLength = + float(formattingCommand.closeIndex - formattingCommand.openIndex); + formattingStack[formattingStack.length] = formattingStackHead; +} + +private final function SwapFormattingStack(Text.Character tagCharacter) +{ + local FormattingInfo updatedFormatting; + if (formattingStack.length > 0) { + updatedFormatting = formattingStackHead; + } + if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.plainColor)) + { + updatedFormatting.colored = true; + updatedFormatting.gradient = false; + } + else { + Report(FSE_BadShortColorTag, _.text.FromString("^" $ Chr(tagCharacter.codePoint))); + } + formattingStackHead = updatedFormatting; + if (formattingStack.length > 0) { + formattingStack[formattingStack.length - 1] = updatedFormatting; + } + else { + formattingStack[0] = updatedFormatting; + } +} + +private final function PopFormattingStack() +{ + // Remove the top of the stack + if (formattingStack.length > 0) { + formattingStack.length = formattingStack.length - 1; + } + // Update the stack head copy + if (formattingStack.length > 0) { + formattingStackHead = formattingStack[formattingStack.length - 1]; + } +} + +private final function FormattingInfo ParseFormattingInfo(Text colorTag) +{ + local int i; + local Parser colorParser; + local Color nextColor; + local array specifiedColors; + local Text.Character tildeCharacter; + local array gradientColors; + local array gradientPoints; + local FormattingInfo resultInfo; + if (colorTag.IsEmpty()) + { + Report(FSE_EmptyColorTag); + return resultInfo; // not colored + } + tildeCharacter = _.text.GetCharacter("~"); + specifiedColors = colorTag.SplitByCharacter(tildeCharacter, true); + for (i = 0; i < specifiedColors.length; i += 1) + { + colorParser = _.text.Parse(specifiedColors[i]); + if (_.color.ParseWith(colorParser, nextColor)) + { + colorParser.Confirm(); + gradientColors[gradientColors.length] = nextColor; + gradientPoints[gradientPoints.length] = ParsePoint(colorParser); + } + else { + Report(FSE_BadColor, specifiedColors[i]); + } + _.memory.Free(colorParser); + } + _.memory.FreeMany(specifiedColors); + gradientPoints = NormalizePoints(gradientPoints); + resultInfo.colored = (gradientColors.length > 0); + resultInfo.gradient = (gradientColors.length > 1); + resultInfo.gradientColors = gradientColors; + resultInfo.gradientPoints = gradientPoints; + if (gradientColors.length > 0) { + resultInfo.plainColor = gradientColors[0]; + } + return resultInfo; +} + +private final function float ParsePoint(Parser parser) +{ + local float point; + local Parser.ParserState initialState; + if (!parser.Ok() || parser.HasFinished()) { + return -1; + } + initialState = parser.GetCurrentState(); + // [Necessary part] Should starts with "[" + if (!parser.Match(P("[")).Ok()) + { + Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); + return -1; + } + // [Necessary part] Try parsing number + parser.MNumber(point).Confirm(); + if (!parser.Ok()) + { + Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); + return -1; + } + // [Optional part] Check if number is a percentage + if (parser.Match(P("%")).Ok()) { + point *= 0.01; + } + // This either confirms state of parsing "%" (on success) + // or reverts to the previous state, just after parsing the number + // (on failure) + parser.Confirm(); + parser.R(); + // [Necessary part] Have to have closing parenthesis + if (!parser.HasFinished()) { + parser.Match(P("]")).Confirm(); + } + // Still return `point`, even if there was no closing parenthesis, + // since that is likely what user wants + if (!parser.Ok()) { + Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); + } + return point; +} +/*FIRST-POPOPOINTS 0.00 -1.00 -1.00 -1.00 1.00 5 +PRE-POPOPOINTS 0.00 -1.00 0.00 -1.00 1.00 5 */ +private final function array NormalizePoints(array points) +{ + local int i, j; + local int negativeSegmentStart, negativeSegmentLength; + local float lowerBound, upperBound; + local bool foundNegative; + if (points.length > 1) + { + points[0] = 0.0; + points[points.length - 1] = 1.0; + } + for (i = 1; i < points.length - 1; i += 1) + { + if (points[i] <= 0 || points[i] > 1 || points[i] <= points[i - 1]) { + points[i] = -1; + } + } + for (i = 1; i < points.length; i += 1) + { + if (foundNegative && points[i] > 0) + { + upperBound = points[i]; + for (j = negativeSegmentStart; j < i; j += 1) + { + points[j] = Lerp( float(j - negativeSegmentStart + 1) / float(negativeSegmentLength + 1), + lowerBound, upperBound); + } + negativeSegmentLength = 0; + } + if (!foundNegative && points[i] < 0) + { + lowerBound = points[i - 1]; + negativeSegmentStart = i; + } + foundNegative = (points[i] < 0); + if (foundNegative) { + negativeSegmentLength += 1; + } + } + return points; +} + +public final function Report( + FormattingErrors.FormattedDataErrorType type, + optional Text cause) +{ + if (errors == none) { + return; + } + errors.Report(type, cause); +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Text/FormattedStrings/FormattingCommandList.uc b/sources/Text/FormattedStrings/FormattingCommandList.uc new file mode 100644 index 0000000..17a779f --- /dev/null +++ b/sources/Text/FormattedStrings/FormattingCommandList.uc @@ -0,0 +1,296 @@ +/** + * Formatted string can be thought of as a string with a sequence of + * formatting-changing commands specified within it (either by opening new + * formatting block, swapping to color with "^" or by closing it and reverting + * to the previous one). + * This objects allows to directly access these commands. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class FormattingCommandList extends AcediaObject + dependson(Text); + +enum FormattingCommandType +{ + // Push more data onto formatted stack + FST_StackPush, + // Pop data from formatted stack + FST_StackPop, + // Swap the top value on the formatting stack + FST_StackSwap +}; + +// Formatted `string` is separated into several (possibly nested) parts, +// each with its own formatting. These can be easily handled with a formatting +// stack: +// * Each time a new section opens ("{ ") we put another, +// current formatting on top of the stack; +// * Each time a section closes ("}") we pop the stack, returning to +// a previous formatting. +// * In a special case of "^" color swap that is supposed to last until +// current block closes we simply swap the color of the formatting on +// top of the stack. +struct FormattingCommand +{ + // Only defined for `FST_StackPush` commands that correspond to section + // openers ("{ "). + // Indices of first and last character belonging to block it opens. + var int openIndex; + var int closeIndex; + // Did this block start by opening or closing formatted part? + // Ignored for the very first block without any formatting. + var FormattingCommandType type; + // Full text inside the block, without any formatting + var array contents; + // Formatting tag for the next block + // (only used for `FST_StackPush` command type) + var MutableText tag; + // Formatting character for the "^"-type tag + // (only used for `FST_StackSwap` command type) + var Text.Character charTag; +}; +// Appending formatted `string` into the `MutableText` first requires its +// transformation into series of `FormattingCommand` and then their +// execution to assemble the `MutableText`. +// First element of `commandList` is special and is used solely as +// a container for unformatted data. It should not be used to execute +// formatting stack commands. +// This variable contains intermediary data. +var private array commandList; + +// Stack that keeps track of which (by index inside `commandList`) command +// opened section we are currently parsing. This is needed to record positions +// at which each block is opened and closed. +var private array pushCommandIndicesStack; +// Store contents for the next command here, because appending array in +// the struct is expensive +var private array currentContents; +// `Parser` used to break input formatted string into commands, only used +// during building this object (inside `BuildSelf()` method). +var private Parser parser; +// `FormattingErrors` object used to reports errors during building process. +// It is "borrowed" - meaning that we do not really own it and should not +// deallocate it. Only set as a field for convenience. +var private FormattingErrors borrowedErrors; + +const CODEPOINT_ESCAPE = 27; // ASCII escape code +const CODEPOINT_OPEN_FORMAT = 123; // '{' +const CODEPOINT_CLOSE_FORMAT = 125; // '}' +const CODEPOINT_FORMAT_ESCAPE = 38; // '&' +const CODEPOINT_ACCENT = 94; // '^' +const CODEPOINT_TILDE = 126; // '~' + +protected function Finalizer() +{ + local int i; + _.memory.Free(parser); + parser = none; + borrowedErrors = none; + if (currentContents.length > 0) { + currentContents.length = 0; + } + if (pushCommandIndicesStack.length > 0) { + pushCommandIndicesStack.length = 0; + } + for (i = 0; i < commandList.length; i += 1) { + _.memory.Free(commandList[i].tag); + } + if (commandList.length > 0) { + commandList.length = 0; + } +} + +/** + * Create `FormattingCommandList` based on given `Text`. + * + * @param input `Text` that should be treated as "formatted" and + * to be broken into formatting commands. + * @param errorsReporter If specified, will be used to report errors + * (can only report `FSE_UnmatchedClosingBrackets`). + * @return New `FormattingCommandList` instance that allows us to have direct + * access to formatting commands. + */ +public final static function FormattingCommandList FromText( + Text input, + optional FormattingErrors errorsReporter) +{ + local FormattingCommandList newList; + newList = FormattingCommandList( + __().memory.Allocate(class'FormattingCommandList')); + newList.parser = __().text.Parse(input); + newList.borrowedErrors = errorsReporter; + newList.BuildSelf(); + __().memory.Free(newList.parser); + newList.parser = none; + newList.borrowedErrors = none; + return newList; +} + +/** + * Returns command with index `commandIndex`. + * + * @param commandIndex Index of the command to return. + * Must be non-negative (`>= 0`) and less than `GetAmount()`. + * @return Command with index `commandIndex`. + * If given `commandIndex` is out of bounds - returns invalid command. + * `tag` field is guaranteed to be non-`none` and should be deallocated. + */ +public final function FormattingCommand GetCommand(int commandIndex) +{ + local MutableText resultTag; + local FormattingCommand result; + if (commandIndex < 0) return result; + if (commandIndex >= commandList.length) return result; + + result = commandList[commandIndex]; + resultTag = result.tag; + if (resultTag != none) { + result.tag = resultTag.MutableCopy(); + } + return result; +} + +/** + * Returns amount of commands inside caller `FormattingCommandList`. + * + * @return Amount of commands inside caller `FormattingCommandList`. + */ +public final function int GetAmount() +{ + return commandList.length; +} + +// Method that turns `parser` into proper `FormattingCommandList` object. +private final function BuildSelf() +{ + //local int i; + local int characterCounter; + local Text.Character nextCharacter; + local FormattingCommand nextCommand; + while (!parser.HasFinished()) + { + parser.MCharacter(nextCharacter); + // New command by "{" + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) + { + nextCommand = AddCommand(nextCommand, FST_StackPush, characterCounter); + parser.MUntil(nextCommand.tag,, true) + .MCharacter(nextCommand.charTag); // Simply to skip a char + continue; + } + // New command by "}" + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) + { + nextCommand = AddCommand(nextCommand, FST_StackPop, characterCounter); + continue; + } + // New command by "^" + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) + { + nextCommand = AddCommand(nextCommand, FST_StackSwap, characterCounter); + parser.MCharacter(nextCommand.charTag); + if (!parser.Ok()) { + break; + } + continue; + } + // Escaped sequence + if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { + parser.MCharacter(nextCharacter); + } + if (!parser.Ok()) { + break; + } + currentContents[currentContents.length] = nextCharacter; + characterCounter += 1; + } + // Only put in empty command if there is nothing else. + if (currentContents.length > 0 || commandList.length == 0) + { + nextCommand.contents = currentContents; + commandList[commandList.length] = nextCommand; + } + /*for (i = 0; i < commandList.length; i += 1) + { + Log(">>>COMMAND LIST FOR" @ i $ "<<<"); + Log("OPEN/CLOSE:" @ commandList[i].openIndex @ "/" @ commandList[i].closeIndex); + Log("TYPE:" @ commandList[i].type); + Log("CONTETS LENGTH:" @ commandList[i].contents.length); + if (commandList[i].tag != none) { + Log("TAG:" @ commandList[i].tag.ToString()); + } + else { + Log("TAG: NONE"); + } + }*/ +} + +// Helper method for a quick creation of a new `FormattingCommand` +private final function FormattingCommand AddCommand( + FormattingCommand nextCommand, + FormattingCommandType newStackCommandType, + optional int currentCharacterIndex) +{ + local int lastPushIndex; + local FormattingCommand newCommand; + nextCommand.contents = currentContents; + if (currentContents.length > 0) { + currentContents.length = 0; + } + commandList[commandList.length] = nextCommand; + if (newStackCommandType == FST_StackPop) + { + lastPushIndex = PopIndex(); + if (lastPushIndex >= 0) { + // BLABLA + commandList[lastPushIndex].closeIndex = currentCharacterIndex - 1; + } + else if (borrowedErrors != none) { + borrowedErrors.Report(FSE_UnmatchedClosingBrackets); + } + } + newCommand.type = newStackCommandType; + if (newStackCommandType == FST_StackPush) + { + newCommand.openIndex = currentCharacterIndex; + newCommand.closeIndex = -1; + // BLABLA + PushIndex(commandList.length); + } + return newCommand; +} + +private final function PushIndex(int index) +{ + pushCommandIndicesStack[pushCommandIndicesStack.length] = + commandList.length; +} + +private final function int PopIndex() +{ + local int result; + if (pushCommandIndicesStack.length <= 0) { + return -1; + } + result = pushCommandIndicesStack[pushCommandIndicesStack.length - 1]; + pushCommandIndicesStack.length = pushCommandIndicesStack.length - 1; + return result; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Text/FormattedStrings/FormattingErrors.uc b/sources/Text/FormattedStrings/FormattingErrors.uc new file mode 100644 index 0000000..1e26256 --- /dev/null +++ b/sources/Text/FormattedStrings/FormattingErrors.uc @@ -0,0 +1,179 @@ +/** + * Simple aggregator object for errors that may arise during parsing of + * formatted string. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class FormattingErrors extends AcediaObject; + +/** + * Errors that can occur during parsing of the formatted string. + */ +enum FormattedDataErrorType +{ + // There was an unmatched closing figure bracket, e.g. + // "{$red Hey} you, there}!" + FSE_UnmatchedClosingBrackets, + // Color tag was empty, e.g. "Why not { just kill them}?" + FSE_EmptyColorTag, + // Color tag cannot be parsed as a color or color gradient, + // e.g. "Why not {just kill them}?" + FSE_BadColor, + // Gradient color tag contained bad point specified, e.g. + // "That is SO {$red~$orange(what?)~$red AMAZING}!!!" or + // "That is SO {$red~$orange(0.76~$red AMAZING}!!!" + FSE_BadGradientPoint, + FSE_BadShortColorTag +}; + +// `FSE_UnmatchedClosingBrackets` and `FSE_EmptyColorTag` errors never have any +// `Text` hint associated with them, so simply store how many times they were +// invoked. +var private int unmatchedClosingBracketsErrorCount; +var private int emptyColorTagErrorCount; +// `FSE_BadColor` and `FSE_BadGradientPoint` are always expected to have +// a `Text` hint reported alongside them, so simply store that hint. +var private array badColorTagErrorHints; +var private array badGradientTagErrorHints; +var private array badShortColorTagErrorHints; + +// We will report accumulated errors as an array of these structs. +struct FormattedDataError +{ + // Type of the error + var FormattedDataErrorType type; + // How many times had this error happened? + // Can be specified for `FSE_UnmatchedClosingBrackets` and + // `FSE_EmptyColorTag` error types. Never negative. + var int count; + // `Text` hint that should help user understand where the error is + // coming from. + // Can be specified for `FSE_BadColor` and `FSE_BadGradientPoint` + // error types. + var Text cause; +}; + +protected function Finalizer() +{ + unmatchedClosingBracketsErrorCount = 0; + emptyColorTagErrorCount = 0; + _.memory.FreeMany(badColorTagErrorHints); + _.memory.FreeMany(badShortColorTagErrorHints); + _.memory.FreeMany(badGradientTagErrorHints); + badColorTagErrorHints.length = 0; + badShortColorTagErrorHints.length = 0; + badGradientTagErrorHints.length = 0; +} + +/** + * Adds new error to the caller `FormattingErrors` object. + * + * @param type Type of the new error. + * @param cause Auxiliary `Text` that might give user additional hint about + * what exactly went wrong. + * If this parameter is `none` for errors `FSE_BadColor` or + * `FSE_BadGradientPoint` - method will do nothing. + */ +public final function Report(FormattedDataErrorType type, optional Text cause) +{ + switch (type) + { + case FSE_UnmatchedClosingBrackets: + unmatchedClosingBracketsErrorCount += 1; + break; + case FSE_EmptyColorTag: + emptyColorTagErrorCount += 1; + break; + case FSE_BadColor: + if (cause != none) { + badColorTagErrorHints[badColorTagErrorHints.length] = cause.Copy(); + } + break; + case FSE_BadShortColorTag: + if (cause != none) + { + badShortColorTagErrorHints[badShortColorTagErrorHints.length] = + cause.Copy(); + } + break; + case FSE_BadGradientPoint: + if (cause != none) + { + badGradientTagErrorHints[badGradientTagErrorHints.length] = + cause.Copy(); + } + break; + } +} + +/** + * Returns array of errors collected so far. + * + * @return Array of errors collected so far. + * Each `FormattedDataError` in array has either non-`none` `cause` field + * or strictly positive `count > 0` field (but not both). + * `count` field is always guaranteed to not be negative. + * WARNING: `FormattedDataError` struct may contain `Text` objects that + * should be deallocated. + */ +public final function array GetErrors() +{ + local int i; + local FormattedDataError newError; + local array errors; + // We overwrite old `cause` in `newError` with new one each time we + // add new error, so it should be fine to not set it to `none` after + // "moving it" into `errors`. + newError.type = FSE_BadColor; + for (i = 0; i < badColorTagErrorHints.length; i += 1) + { + newError.cause = badColorTagErrorHints[i].Copy(); + errors[errors.length] = newError; + } + newError.type = FSE_BadShortColorTag; + for (i = 0; i < badShortColorTagErrorHints.length; i += 1) + { + newError.cause = badShortColorTagErrorHints[i].Copy(); + errors[errors.length] = newError; + } + newError.type = FSE_BadGradientPoint; + for (i = 0; i < badGradientTagErrorHints.length; i += 1) + { + newError.cause = badGradientTagErrorHints[i].Copy(); + errors[errors.length] = newError; + } + // Need to reset `cause` here, to avoid duplicating it in + // following two errors + newError.cause = none; + if (unmatchedClosingBracketsErrorCount > 0) + { + newError.type = FSE_UnmatchedClosingBrackets; + newError.count = unmatchedClosingBracketsErrorCount; + errors[errors.length] = newError; + } + if (emptyColorTagErrorCount > 0) + { + newError.type = FSE_EmptyColorTag; + newError.count = emptyColorTagErrorCount; + errors[errors.length] = newError; + } + return errors; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index 821a2d6..dca84d3 100644 --- a/sources/Text/MutableText.uc +++ b/sources/Text/MutableText.uc @@ -1,6 +1,6 @@ /** * Mutable version of Acedia's `Text` - * Copyright 2020 - 2021 Anton Tarasenko + * Copyright 2020 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -19,59 +19,7 @@ */ class MutableText extends Text; -var private int CODEPOINT_NEWLINE, CODEPOINT_ACCENT; - -enum FormattedStackCommandType -{ - // Push more data onto formatted stack - FST_StackPush, - // Pop data from formatted stack - FST_StackPop, - // Swap the top value on the formatting stack - FST_StackSwap -}; - -// Formatted `string` is separated into several (possibly nested) parts, -// each with its own formatting. These can be easily handled with a formatting -// stack: -// * Each time a new section opens ("{ contents; - // Formatting tag for the next block - // (only used for `FST_StackPush` command type) - var MutableText tag; - // Formatting character for the "^"-type tag - // (only used for `FST_StackSwap` command type) - var Character charTag; -}; -// Appending formatted `string` into the `MutableText` first requires its -// transformation into series of `FormattedStackCommand` and then their -// execution to assemble the `MutableText`. -// First element of `stackCommands` is special and is used solely as -// a container for unformatted data. It should not be used to execute -// formatting stack commands. -// This variable contains intermediary data. -var array stackCommands; -// Formatted `string` can have an arbitrary level of folded format definitions, -// this array is used as a stack to keep track of opened formatting blocks -// when appending formatted `string`. -var array formattingStack; +var private int CODEPOINT_NEWLINE; /** * Clears all current data from the caller `MutableText` instance. @@ -84,6 +32,55 @@ public final function MutableText Clear() return self; } +/** + * Appends a new character to the caller `MutableText`, while discarding its + * own formatting. + * + * @param newCharacter Character to add to the caller `MutableText`. + * Only valid characters will be added. Its formatting will be discarded. + * @param characterFormatting You can use this parameter to specify formatting + * `newCharacter` should have in the caller `MutableText` instead of + * its own. + * @return Caller `MutableText` to allow for method chaining. + */ +public final function MutableText AppendRawCharacter( + Text.Character newCharacter, + optional Formatting characterFormatting) +{ + if (!_.text.IsValidCharacter(newCharacter)) { + return self; + } + SetFormatting(characterFormatting); + return MutableText(AppendCodePoint(newCharacter.codePoint)); +} + +/** + * Appends all characters from the given array, in order, but discarding their + * own formatting. + * + * This method should be faster than `AppendManyCharacters()` or several calls + * of `AppendRawCharacter()`, since it does not need to check whether + * formatting is changed from character to character. + * + * @param newCharacters Characters to be added to the caller `MutableText`. + * Only valid characters will be added. Their formatting will be discarded. + * @param characterFormatting You can use this parameter to specify formatting + * `newCharacters` should have in the caller `MutableText` instead of + * their own. + * @return Caller `MutableText` to allow for method chaining. + */ +public final function MutableText AppendManyRawCharacters( + array newCharacters, + optional Formatting charactersFormatting) +{ + local int i; + SetFormatting(charactersFormatting); + for (i = 0; i < newCharacters.length; i += 1) { + AppendCodePoint(newCharacters[i].codePoint); + } + return self; +} + /** * Appends a new character to the caller `MutableText`. * @@ -100,6 +97,23 @@ public final function MutableText AppendCharacter(Text.Character newCharacter) return MutableText(AppendCodePoint(newCharacter.codePoint)); } +/** + * Appends all characters from the given array, in order. + * + * @param newCharacters Characters to be added to the caller `MutableText`. + * Only valid characters will be added. + * @return Caller `MutableText` to allow for method chaining. + */ +public final function MutableText AppendManyCharacters( + array newCharacters) +{ + local int i; + for (i = 0; i < newCharacters.length; i += 1) { + AppendCharacter(newCharacters[i]); + } + return self; +} + /** * Adds new line character to the end of the caller `MutableText`. * @@ -271,10 +285,14 @@ public final function MutableText AppendFormatted( Text source, optional Formatting defaultFormatting) { - local Parser parser; - parser = _.text.Parse(source); - AppendFormattedParser(parser, defaultFormatting); - parser.FreeSelf(); + // TODO: is this the best way? + local Text appendedPart; + local FormattedStringData data; + data = class'FormattedStringData'.static.FromText(source); + appendedPart = data.GetResult(); + Append(appendedPart); + _.memory.Free(appendedPart); + _.memory.Free(data); return self; } @@ -291,174 +309,14 @@ public final function MutableText AppendFormattedString( string source, optional Formatting defaultFormatting) { - local Parser parser; - parser = _.text.ParseString(source); - AppendFormattedParser(parser, defaultFormatting); - parser.FreeSelf(); + // TODO: is this the best way? + local Text sourceAsText; + sourceAsText = _.text.FromString(source); + AppendFormatted(sourceAsText); + _.memory.Free(sourceAsText); return self; } -/** - * Appends contents of the formatted `string` to the caller `MutableText`. - * - * @param source Formatted `string` to be appended to - * the caller `MutableText`. - * @param defaultFormatting Formatting to be used for `source`'s characters - * that have no color information defined. - * @return Caller `MutableText` to allow for method chaining. - */ -private final function MutableText AppendFormattedParser( - Parser sourceParser, - optional Formatting defaultFormatting) -{ - local int i; - local Parser tagParser; - BuildFormattingStackCommands(sourceParser); - if (stackCommands.length <= 0) { - return self; - } - SetupFormattingStack(defaultFormatting); - tagParser = Parser(_.memory.Allocate(class'Parser')); - SetFormatting(defaultFormatting); - // First element of color stack is special and has no color information; - // see `BuildFormattingStackCommands()` for details. - AppendManyCodePoints(stackCommands[0].contents); - for (i = 1; i < stackCommands.length; i += 1) - { - if (stackCommands[i].type == FST_StackPush) - { - tagParser.Initialize(stackCommands[i].tag); - SetFormatting(PushIntoFormattingStack(tagParser)); - } - else if (stackCommands[i].type == FST_StackPop) { - SetFormatting(PopFormattingStack()); - } - else if (stackCommands[i].type == FST_StackSwap) { - SetFormatting(SwapFormattingStack(stackCommands[i].charTag)); - } - AppendManyCodePoints(stackCommands[i].contents); - _.memory.Free(stackCommands[i].tag); - } - stackCommands.length = 0; - _.memory.Free(tagParser); - return self; -} - -// Function that parses formatted `string` into array of -// `FormattedStackCommand`s. -// Returned array is guaranteed to always have at least one element. -// First element in array always corresponds to part of the input string -// (`source`) without any formatting defined, even if it's empty. -// This is to avoid having fourth command type, only usable at the beginning. -private final function BuildFormattingStackCommands(Parser parser) -{ - local Character nextCharacter; - local FormattedStackCommand nextCommand; - stackCommands.length = 0; - while (!parser.HasFinished()) - { - parser.MCharacter(nextCharacter); - // New command by "{" - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) - { - stackCommands[stackCommands.length] = nextCommand; - nextCommand = CreateStackCommand(FST_StackPush); - parser.MUntil(nextCommand.tag,, true) - .MCharacter(nextCommand.charTag); // Simply to skip a char - continue; - } - // New command by "}" - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) - { - stackCommands[stackCommands.length] = nextCommand; - nextCommand = CreateStackCommand(FST_StackPop); - continue; - } - // New command by "^" - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) - { - stackCommands[stackCommands.length] = nextCommand; - nextCommand = CreateStackCommand(FST_StackSwap); - parser.MCharacter(nextCommand.charTag); - if (!parser.Ok()) { - break; - } - continue; - } - // Escaped sequence - if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { - parser.MCharacter(nextCharacter); - } - if (!parser.Ok()) { - break; - } - nextCommand.contents[nextCommand.contents.length] = - nextCharacter.codePoint; - } - // Only put in empty command if there is nothing else. - if (nextCommand.contents.length > 0 || stackCommands.length == 0) { - stackCommands[stackCommands.length] = nextCommand; - } -} - -// Following four functions are to maintain a "color stack" that will -// remember unclosed colors (new colors are obtained from formatting commands -// sequence) defined in formatted string, in order. -// Stack array always contains one element, defined by -// the `SetupFormattingStack()` call. It corresponds to the default formatting -// that will be used when we pop all the other elements. -// It is necessary to deal with possible folded formatting definitions in -// formatted strings. -private final function SetupFormattingStack(Text.Formatting defaultFormatting) -{ - formattingStack.length = 0; - formattingStack[0] = defaultFormatting; -} - -private final function Formatting PushIntoFormattingStack( - Parser formattingDefinitionParser) -{ - local Formatting newFormatting; - if (_.color.ParseWith(formattingDefinitionParser, newFormatting.color)) { - newFormatting.isColored = true; - } - formattingStack[formattingStack.length] = newFormatting; - return newFormatting; -} - -private final function Formatting SwapFormattingStack(Character tagCharacter) -{ - local Formatting updatedFormatting; - if (formattingStack.length <= 0) { - return updatedFormatting; - } - updatedFormatting = formattingStack[formattingStack.length - 1]; - if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.color)) { - updatedFormatting.isColored = true; - } - formattingStack[formattingStack.length - 1] = updatedFormatting; - return updatedFormatting; -} - -private final function Formatting PopFormattingStack() -{ - local Formatting result; - formattingStack.length = Max(1, formattingStack.length - 1); - if (formattingStack.length > 0) { - result = formattingStack[formattingStack.length - 1]; - } - return result; -} - -// Helper method for a quick creation of a new `FormattedStackCommand` -private final function FormattedStackCommand CreateStackCommand( - FormattedStackCommandType stackCommandType) -{ - local FormattedStackCommand newCommand; - newCommand.type = stackCommandType; - return newCommand; -} - /** * Unlike `Text`, `MutableText` can change it's content and therefore it's * hash code cannot depend on it. So we restore `AcediaObject`'s behavior and @@ -679,6 +537,5 @@ public final function MutableText Simplify(optional bool fixInnerSpacings) defaultproperties { - CODEPOINT_NEWLINE = 10 - CODEPOINT_ACCENT = 94 + CODEPOINT_NEWLINE = 10 } \ No newline at end of file diff --git a/sources/Text/Parser.uc b/sources/Text/Parser.uc index 04a0945..f02d12c 100644 --- a/sources/Text/Parser.uc +++ b/sources/Text/Parser.uc @@ -1,7 +1,7 @@ /** * Implements a simple `Parser` with built-in functions to parse simple * UnrealScript's types and support for saving / restoring parser states. - * Copyright 2021 Anton Tarasenko + * Copyright 2021-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -34,7 +34,7 @@ var public int CODEPOINT_ULARGE; var private Text content; // Incremented each time `Parser` is reinitialized with new `content`. // Can be used to make `Parser` object completely independent from -// it's past after each re-initialization. +// its past after each re-initialization. // This helps to avoid needless reallocations. var private int version; @@ -126,7 +126,7 @@ public final function Parser InitializeS(string source) * Checks if `Parser` is in a failed state. * * Parser enters a failed state whenever any parsing call returns without - * completing it's job. `Parser` in a failed state will automatically fail + * completing its job. `Parser` in a failed state will automatically fail * any further parsing attempts until it gets reset via `R()` call. * * @return Returns 'false' if `Parser()` is in a failed state and @@ -141,7 +141,7 @@ public final function bool Ok() * Returns copy of the current state of this parser. * * As long as caller `Parser` was not reinitialized, returned `ParserState` - * structure can be used to revert this `Parser` to it's current condition + * structure can be used to revert this `Parser` to its current condition * by a `RestoreState()` call. * * @see `RestoreState()` @@ -156,7 +156,7 @@ public final function ParserState GetCurrentState() * Returns copy of (currently) last confirmed state of this parser. * * As long as caller `Parser` was not reinitialized, returned `ParserState` - * structure can be used to revert this `Parser` to it's current confirmed + * structure can be used to revert this `Parser` to its current confirmed * state by a `RestoreState()` call. * * @see `RestoreState()`, `Confirm()`, `R()` @@ -253,7 +253,7 @@ public final function bool Confirm() /** * Resets `Parser` to a last state recorded as confirmed by a last successful * `Confirm()` function call. If there weren't any such call - - * reverts `Parser` to it's state right after initialization. + * reverts `Parser` to its state right after initialization. * * Always resets failed state of a `Parser`. Cannot fail. * @@ -302,7 +302,7 @@ protected final function Parser ShiftPointer(optional int shift) * and does not fit `Parser`'s contents, returns invalid character. * `GetCodePoint()` with default (`0`) parameter can also return * invalid character if caller `Parser` was not initialized, - * it's contents are empty or it has already consumed all input. + * its contents are empty or it has already consumed all input. */ protected final function Text.Character GetCharacter(optional int shift) { @@ -361,7 +361,7 @@ public final function int GetRemainingLength() } /** - * Checks if caller `Parser` has already parsed all of it's content. + * Checks if caller `Parser` has already parsed all of its content. * Uninitialized `Parser` has no content and, therefore, parsed it all. * * Should return `true` iff `GetRemainingLength() == 0`. @@ -668,6 +668,72 @@ public final function Parser MEscapedSequence( return self; } +/** + * Attempts to parse a "name": a string literal that: + * 1. Contains only digits and latin characters; + * 2. Starts with a latin character. + * These restrictions help to avoid possible issues that arise from having + * different code pages and are used. For example, only "names" are considered + * to be valid aliases. + * + * @param result If parsing is successful, this `MutableText` will contain + * the contents of the matched "name", if parsing has failed, its value + * is undefined. Any passed contents are simply discarded. + * If passed `MutableText` equals to `none`, new instance will be + * automatically allocated. This will be done regardless of whether + * parsing fails. + * @return Returns the caller `Parser`, to allow for function chaining. + */ +public final function Parser MName(out MutableText result) +{ + local TextAPI api; + local Text.Character nextCharacter; + ResetResultText(result); + if (!Ok()) return self; + if (GetRemainingLength() <= 0) return Fail(); + api = _.text; + nextCharacter = GetCharacter(); + if (!api.IsAlpha(nextCharacter)) return Fail(); + + result.AppendCharacter(nextCharacter); + ShiftPointer(); + nextCharacter = GetCharacter(); + while (api.IsAlpha(nextCharacter) || api.IsDigit(nextCharacter)) + { + result.AppendCharacter(nextCharacter); + ShiftPointer(); + nextCharacter = GetCharacter(); + } + return self; +} + +/** + * Attempts to parse a "name": a string literal that: + * 1. Contains only digits and latin characters; + * 2. Starts with a latin character. + * These restrictions help to avoid possible issues that arise from having + * different code pages and are used. For example, only "names" are considered + * to be valid aliases. + * + * @param result If parsing is successful, this `string` will contain the + * contents of the matched "name" with resolved escaped sequences; + * if parsing has failed, its value is undefined. + * Any passed contents are simply discarded. + * @return Returns the caller `Parser`, to allow for function chaining. + */ +public final function Parser MNameS(out string result) +{ + local MutableText wrapper; + if (!Ok()) return self; + + wrapper = _.text.Empty(); + if (MName(wrapper).Ok()) { + result = wrapper.ToString(); + } + wrapper.FreeSelf(); + return self; +} + /** * Attempts to parse a string literal: a string enclosed in either of * the following quotation marks: ", ', `. @@ -678,7 +744,7 @@ public final function Parser MEscapedSequence( * * @param result If parsing is successful, this `MutableText` will contain * the contents of string literal with resolved escaped sequences; - * if parsing has failed, it's value is undefined. + * if parsing has failed, its value is undefined. * Any passed contents are simply discarded. * If passed `MutableText` equals to `none`, new instance will be * automatically allocated. This will be done regardless of whether @@ -736,7 +802,7 @@ public final function Parser MStringLiteral(out MutableText result) * * @param result If parsing is successful, this `string` will contain the * contents of string literal with resolved escaped sequences; - * if parsing has failed, it's value is undefined. + * if parsing has failed, its value is undefined. * Any passed contents are simply discarded. * @return Returns the caller `Parser`, to allow for function chaining. */ diff --git a/sources/Text/Tests/TEST_FormattedStrings.uc b/sources/Text/Tests/TEST_FormattedStrings.uc new file mode 100644 index 0000000..caeeee3 --- /dev/null +++ b/sources/Text/Tests/TEST_FormattedStrings.uc @@ -0,0 +1,565 @@ +/** + * Set of tests for functionality of parsing formatted strings. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class TEST_FormattedStrings extends TestCase + abstract; + +protected static function MutableText GetRGBText( + optional bool noFormattingReset) +{ + local int wordIndex; + local MutableText result; + result = __().text.FromStringM("This is red, green and blue!"); + wordIndex = result.IndexOf(P("red")); + result.ChangeFormatting(__().text.FormattingFromColor(__().color.red), + wordIndex, 3); + wordIndex = result.IndexOf(P("green")); + result.ChangeFormatting(__().text.FormattingFromColor(__().color.lime), + wordIndex, 5); + wordIndex = result.IndexOf(P("blue")); + result.ChangeFormatting(__().text.FormattingFromColor(__().color.blue), + wordIndex, 4); + // also color ", " and " and " parts white, and "!" part blue + if (noFormattingReset) + { + result.ChangeFormatting(__().text.FormattingFromColor(__().color.blue), + wordIndex, -1); + wordIndex = result.IndexOf(P(", ")); + result.ChangeFormatting(__().text.FormattingFromColor(__().color.white), + wordIndex, 2); + wordIndex = result.IndexOf(P(" and ")); + result.ChangeFormatting(__().text.FormattingFromColor(__().color.white), + wordIndex, 5); + } + return result; +} + +protected static function TESTS() +{ + Test_Simple(); + Test_Gradient(); + Test_Errors(); +} + +protected static function Test_Errors() +{ + Context("Testing error reporting for formatted strings."); + SubTest_ErrorUnmatchedClosingBrackets(); + SubTest_ErrorEmptyColorTag(); + SubTest_ErrorBadColor(); + SubTest_ErrorBadShortColorTag(); + SubTest_ErrorBadGradientPoint(); + SubTest_AllErrors(); +} + +protected static function SubTest_ErrorUnmatchedClosingBrackets() +{ + local array errors; + local FormattedStringData data; + Issue("Unmatched closing brackets are not reported."); + data = class'FormattedStringData'.static.FromText(P("Testing {$pink pink text}}!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + data = class'FormattedStringData'.static.FromText(P("Testing regular text!}"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + data = class'FormattedStringData'.static + .FromText(P("This is {rgb(255,0,0) red{rgb(255,255,255) , }}}" + $ "{rgb(0,255,0) gr}een{rgb(255,255,255) and }}}}{rgb(0,0,255)" + $ " blue!}}}"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_UnmatchedClosingBrackets); + TEST_ExpectTrue(errors[0].count == 6); + TEST_ExpectNone(errors[0].cause); +} + +protected static function SubTest_ErrorEmptyColorTag() +{ + local array errors; + local FormattedStringData data; + Issue("Empty color tags are not reported."); + data = class'FormattedStringData'.static.FromText(P("Testing { pink text}!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + data = class'FormattedStringData'.static.FromText(P("Testing {$red regu{ lar tex}t!}"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); + TEST_ExpectTrue(errors[0].count == 1); + TEST_ExpectNone(errors[0].cause); + data = class'FormattedStringData'.static + .FromText(P("This is { {rgb(255,255,255)~$green , }" + $ "{#800c37 ^ggre^gen{ and }}}^bblue!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_EmptyColorTag); + TEST_ExpectTrue(errors[0].count == 2); + TEST_ExpectNone(errors[0].cause); +} + +protected static function SubTest_ErrorBadColor() +{ + local array errors; + local FormattedStringData data; + Issue("Bad color is not reported."); + data = class'FormattedStringData'.static.FromText(P("Testing {$cat pink text}!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_BadColor); + TEST_ExpectTrue(errors[0].cause.ToString() == "$cat"); + TEST_ExpectTrue(errors[0].count == 0); + data = class'FormattedStringData'.static.FromText(P("Testing {dog regular} {#wicked text!}"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 2); + TEST_ExpectTrue(errors[0].type == FSE_BadColor); + TEST_ExpectTrue(errors[1].type == FSE_BadColor); + TEST_ExpectTrue(errors[0].cause.ToString() == "dog"); + TEST_ExpectTrue(errors[1].cause.ToString() == "#wicked"); + data = class'FormattedStringData'.static + .FromText(P("This is {goat red{rgb(255,255,255)~lol~$green , }" + $ "{#800c37 ^ggre^gen{324sd and }}}^bblue!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 3); + TEST_ExpectTrue(errors[0].type == FSE_BadColor); + TEST_ExpectTrue(errors[1].type == FSE_BadColor); + TEST_ExpectTrue(errors[2].type == FSE_BadColor); + TEST_ExpectTrue(errors[0].cause.ToString() == "goat"); + TEST_ExpectTrue(errors[1].cause.ToString() == "lol"); + TEST_ExpectTrue(errors[2].cause.ToString() == "324sd"); +} + +protected static function SubTest_ErrorBadShortColorTag() +{ + local array errors; + local FormattedStringData data; + Issue("Bad short color tag is not reported."); + data = class'FormattedStringData'.static.FromText(P("This is ^xred^w, ^ugreen^x and ^zblue!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 4); + TEST_ExpectTrue(errors[0].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[0].cause.ToString() == "^x"); + TEST_ExpectTrue(errors[0].count == 0); + TEST_ExpectTrue(errors[1].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[1].cause.ToString() == "^u"); + TEST_ExpectTrue(errors[1].count == 0); + TEST_ExpectTrue(errors[2].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[2].cause.ToString() == "^x"); + TEST_ExpectTrue(errors[2].count == 0); + TEST_ExpectTrue(errors[3].type == FSE_BadShortColorTag); + TEST_ExpectTrue(errors[3].cause.ToString() == "^z"); + TEST_ExpectTrue(errors[3].count == 0); +} + +protected static function SubTest_ErrorBadGradientPoint() +{ + local array errors; + local FormattedStringData data; + Issue("Bad gradient point is not reported."); + data = class'FormattedStringData'.static.FromText(P("Testing {$pink[dog] pink text}!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 1); + TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[0].cause.ToString() == "[dog]"); + TEST_ExpectTrue(errors[0].count == 0); + data = class'FormattedStringData'.static.FromText(P("Testing {45,2,241[bad] regular} {#ffaacd~rgb(2,3,4)45worse] text!}"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 2); + TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[1].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[0].cause.ToString() == "[bad]"); + TEST_ExpectTrue(errors[1].cause.ToString() == "45worse]"); + data = class'FormattedStringData'.static + .FromText(P("This is {$red[45%%] red{rgb(255,255,255)~45,3,128point~$green , }" + $ "{#800c37 ^ggre^gen{#43fa6b3c and }}}^bblue!"), true); + errors = data.BorrowErrors().GetErrors(); + TEST_ExpectTrue(errors.length == 3); + TEST_ExpectTrue(errors[0].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[1].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[2].type == FSE_BadGradientPoint); + TEST_ExpectTrue(errors[0].cause.ToString() == "[45%%]"); + TEST_ExpectTrue(errors[1].cause.ToString() == "point"); + TEST_ExpectTrue(errors[2].cause.ToString() == "3c"); +} + +protected static function SubTest_AllErrors() +{ + local int i; + local bool foundUnmatched, foundEmpty, foundBadColor, foundBadPoint, foundBadShortTag; + local array errors; + local FormattedStringData data; + Issue("COMPLEX."); + data = class'FormattedStringData'.static.FromText(P("This} is {$cat~$green[%7] red{$white , }{ green^z and }}{$blue blue!}}"), true); + errors = data.BorrowErrors().GetErrors(); + for (i = 0; i < errors.length; i += 1) + { + if (errors[i].type == FSE_UnmatchedClosingBrackets) + { + foundUnmatched = true; + TEST_ExpectTrue(errors[i].count == 2); + } + if (errors[i].type == FSE_EmptyColorTag) + { + foundEmpty = true; + TEST_ExpectTrue(errors[i].count == 1); + } + if (errors[i].type == FSE_BadColor) + { + foundBadColor = true; + TEST_ExpectTrue(errors[i].cause.ToString() == "$cat"); + } + if (errors[i].type == FSE_BadGradientPoint) + { + foundBadPoint = true; + TEST_ExpectTrue(errors[i].cause.ToString() == "[%7]"); + } + if (errors[i].type == FSE_BadShortColorTag) + { + foundBadShortTag = true; + TEST_ExpectTrue(errors[i].cause.ToString() == "^z"); + } + } +}//^z, $cat, [%7] +/* // There was an unmatched closing figure bracket, e.g. + // "{$red Hey} you, there}!" + FSE_UnmatchedClosingBrackets, + // Color tag was empty, e.g. "Why not { just kill them}?" + FSE_EmptyColorTag, + // Color tag cannot be parsed as a color or color gradient, + // e.g. "Why not {just kill them}?" + FSE_BadColor, + // Gradient color tag contained bad point specified, e.g. + // "That is SO {$red~$orange(what?)~$red AMAZING}!!!" or + // "That is SO {$red~$orange(0.76~$red AMAZING}!!!" + FSE_BadGradientPoint, + FSE_BadShortColorTag */ +protected static function Test_Simple() +{ + Context("Testing parsing formatted strings with plain colors."); + SubTest_SimpleNone(); + SubTest_SimpleAlias(); + SubTest_SimpleRGB(); + SubTest_SimpleHEX(); + SubTest_SimpleTag(); + SubTest_SimpleMix(); +} + +protected static function SubTest_SimpleNone() +{ + local FormattedStringData data; + Issue("Empty formatted strings are handled incorrectly."); + data = class'FormattedStringData'.static.FromText(P("")); + TEST_ExpectNotNone(data.GetResult()); + TEST_ExpectTrue(data.GetResult().IsEmpty()); + + Issue("Formatted strings with no content are handled incorrectly."); + data = class'FormattedStringData'.static.FromText(P("{$red }")); + TEST_ExpectNotNone(data.GetResult()); + TEST_ExpectTrue(data.GetResult().IsEmpty()); + data = class'FormattedStringData'.static + .FromText(P("{#ff03a5 {$blue }}^3{$lime }")); + TEST_ExpectNotNone(data.GetResult()); + TEST_ExpectTrue(data.GetResult().IsEmpty()); +} + +protected static function SubTest_SimpleAlias() +{ + local MutableText example; + local FormattedStringData data; + Issue("Formatted strings with aliases are handled incorrectly."); + example = GetRGBText(); + data = class'FormattedStringData'.static + .FromText(P("This is {$red red}, {$lime green} and {$blue blue}!")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + example = GetRGBText(true); + data = class'FormattedStringData'.static + .FromText(P("This is {$red red{$white , }{$lime green{$white and }}}" + $ "{$blue blue!}")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); +} + +protected static function SubTest_SimpleRGB() +{ + local MutableText example; + local FormattedStringData data; + Issue("Formatted strings with rgb definitions are handled incorrectly."); + example = GetRGBText(); + data = class'FormattedStringData'.static + .FromText(P("This is {rgb(255,0,0) red}, {rgb(0,255,0) green} and" + @ "{rgb(0,0,255) blue}!")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + example = GetRGBText(true); + data = class'FormattedStringData'.static + .FromText(P("This is {rgb(255,0,0) red{rgb(255,255,255) , }" + $ "{rgb(0,255,0) green{rgb(255,255,255) and }}}{rgb(0,0,255)" + $ " blue!}")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); +} + +protected static function SubTest_SimpleHEX() +{ + local MutableText example; + local FormattedStringData data; + Issue("Formatted strings with hex definitions are handled incorrectly."); + example = GetRGBText(); + data = class'FormattedStringData'.static + .FromText(P("This is {#ff0000 red}, {#00ff00 green} and" + @ "{#0000ff blue}!")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + example = GetRGBText(true); + data = class'FormattedStringData'.static + .FromText(P("This is {#ff0000 red{#ffffff , }" + $ "{#00ff00 green{#ffffff and }}}{#0000ff blue!}")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); +} + +protected static function SubTest_SimpleTag() +{ + local MutableText example; + local FormattedStringData data; + Issue("Formatted strings with rag definitions are handled incorrectly."); + example = GetRGBText(true); + data = class'FormattedStringData'.static + .FromText(P("This is ^rred^w, ^2green^w and ^4blue!")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); +} + +protected static function SubTest_SimpleMix() +{ + local MutableText example; + local FormattedStringData data; + Issue("Formatted strings with mixed definitions are handled incorrectly."); + example = GetRGBText(); + data = class'FormattedStringData'.static + .FromText(P("This is {rgb(255,0,0) red}, {$lime green} and" + @ "{#af4378 ^bblue}!")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); + example = GetRGBText(true); + data = class'FormattedStringData'.static + .FromText(P("This is {$red red{rgb(255,255,255) , }" + $ "{#800c37d ^ggre^gen{#ffffff and }}}^bblue!")); + TEST_ExpectTrue(example.Compare(data.GetResult(),, SFORM_SENSITIVE)); +} + +protected static function Test_Gradient() +{ + Context("Testing parsing formatted strings with gradient."); + SubTest_TestGradientTwoColors(); + SubTest_TestGradientThreeColors(); + SubTest_TestGradientFiveColors(); + SubTest_TestGradientPoints(); + SubTest_TestGradientPointsBad(); +} + +protected static function SubTest_TestGradientTwoColors() +{ + local int i; + local Text result; + local FormattedStringData data; + local Color previousColor, currentColor; + Issue("Simple (two color) gradient block does not color intermediate" + @ "characters correctly."); + data = class'FormattedStringData'.static + .FromText(P("{rgb(255,128,56)~rgb(0,255,56)" + @ "Simple shit to test out gradient}")); + result = data.GetResult(); + previousColor = result.GetFormatting(0).color; + TEST_ExpectTrue(result.GetFormatting(0).isColored); + for (i = 1; i < result.GetLength(); i += 1) + { + TEST_ExpectTrue(result.GetFormatting(i).isColored); + currentColor = result.GetFormatting(i).color; + TEST_ExpectTrue(previousColor.r > currentColor.r); + TEST_ExpectTrue(previousColor.g < currentColor.g); + TEST_ExpectTrue(previousColor.b == currentColor.b); + previousColor = currentColor; + } + Issue("Gradient (two color) block does not color edge characters" + @ "correctly."); + previousColor = result.GetFormatting(0).color; + currentColor = result.GetFormatting(result.GetLength() - 1).color; + TEST_ExpectTrue(previousColor.r == 255); + TEST_ExpectTrue(previousColor.g == 128); + TEST_ExpectTrue(previousColor.b == 56); + TEST_ExpectTrue(currentColor.r == 0); + TEST_ExpectTrue(currentColor.g == 255); + TEST_ExpectTrue(currentColor.b == 56); +} + +protected static function CheckRedDecrease(Text sample, int from, int to) +{ + local int i; + local Color previousColor, currentColor; + previousColor = sample.GetFormatting(from).color; + TEST_ExpectTrue(sample.GetFormatting(from).isColored); + for (i = from + 1; i < to; i += 1) + { + TEST_ExpectTrue(sample.GetFormatting(i).isColored); + currentColor = sample.GetFormatting(i).color; + TEST_ExpectTrue(previousColor.r > currentColor.r); + previousColor = currentColor; + } +} + +protected static function CheckRedIncrease(Text sample, int from, int to) +{ + local int i; + local Color previousColor, currentColor; + previousColor = sample.GetFormatting(from).color; + TEST_ExpectTrue(sample.GetFormatting(from).isColored); + for (i = from + 1; i < to; i += 1) + { + TEST_ExpectTrue(sample.GetFormatting(i).isColored); + currentColor = sample.GetFormatting(i).color; + TEST_ExpectTrue(previousColor.r < currentColor.r); + previousColor = currentColor; + } +} + +protected static function SubTest_TestGradientThreeColors() +{ + local Text result; + local FormattedStringData data; + local Color borderColor; + Issue("Gradient block with three colors does not color intermediate" + @ "characters correctly."); + data = class'FormattedStringData'.static + .FromText(P("{rgb(255,0,0)~#000000~$red" + @ "Simple shit to test out gradient!}")); + result = data.GetResult(); + CheckRedDecrease(result, 0, 16); + CheckRedIncrease(result, 17, result.GetLength()); + Issue("Gradient block with three colors does not color edge characters" + @ "correctly."); + borderColor = result.GetFormatting(0).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(result.GetLength() - 1).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(16).color; + TEST_ExpectTrue(borderColor.r == 0); +} + +protected static function SubTest_TestGradientFiveColors() +{ + local Text result; + local FormattedStringData data; + local Color borderColor; + Issue("Gradient block with five colors does not color intermediate" + @ "characters correctly."); + data = class'FormattedStringData'.static + .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(200,0,0)~rgb(180,0,0)~rgb(210,0,0)~rgb(97,0,0) Go f yourself}!?!?!"));//27 SHIFT + result = data.GetResult(); + CheckRedDecrease(result, 0 + 27, 6 + 27); + CheckRedIncrease(result, 7 + 27, 9 + 27); + CheckRedDecrease(result, 9 + 27, 12 + 27); + Issue("Gradient block with five colors does not color edge characters" + @ "correctly."); + borderColor = result.GetFormatting(0 + 27).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(3 + 27).color; + TEST_ExpectTrue(borderColor.r == 200); + borderColor = result.GetFormatting(6 + 27).color; + TEST_ExpectTrue(borderColor.r == 180); + borderColor = result.GetFormatting(9 + 27).color; + TEST_ExpectTrue(borderColor.r == 210); + borderColor = result.GetFormatting(12 + 27).color; + TEST_ExpectTrue(borderColor.r == 97); +} + +protected static function SubTest_TestGradientPoints() +{ + local Text result; + local FormattedStringData data; + local Color borderColor; + Issue("Gradient points are incorrectly handled."); + data = class'FormattedStringData'.static + .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(0,0,0)[25%]~rgb(123,0,0) Go f yourself}!?!?!")); + result = data.GetResult(); + CheckRedDecrease(result, 0 + 27, 3 + 27); + CheckRedIncrease(result, 3 + 27, 12 + 27); + borderColor = result.GetFormatting(0 + 27).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(3 + 27).color; + TEST_ExpectTrue(borderColor.r == 0); + borderColor = result.GetFormatting(12 + 27).color; + TEST_ExpectTrue(borderColor.r == 123); + Issue("Gradient block does not color intermediate characters correctly."); + data = class'FormattedStringData'.static + .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(0,0,0)[0.75]~rgb(45,0,0) Go f yourself}!?!?!")); + result = data.GetResult(); + CheckRedDecrease(result, 0 + 27, 9 + 27); + CheckRedIncrease(result, 9 + 27, 12 + 27); + borderColor = result.GetFormatting(0 + 27).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(9 + 27).color; + TEST_ExpectTrue(borderColor.r == 0); + borderColor = result.GetFormatting(12 + 27).color; + TEST_ExpectTrue(borderColor.r == 45); +} + +protected static function SubTest_TestGradientPointsBad() +{ + local Text result; + local FormattedStringData data; + local Color borderColor; + Issue("Bad gradient points are incorrectly handled."); + data = class'FormattedStringData'.static + .FromText(P("Check this wacky shit out: {rgb(255,0,0)~rgb(128,0,0)[50%]~rgb(150,0,0)[0.3]~rgb(123,0,0) Go f yourself}!?!?!")); + result = data.GetResult(); + CheckRedDecrease(result, 0 + 27, 6 + 27); + CheckRedIncrease(result, 6 + 27, 9 + 27); + CheckRedDecrease(result, 9 + 27, 12 + 27); + borderColor = result.GetFormatting(0 + 27).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(6 + 27).color; + TEST_ExpectTrue(borderColor.r == 128); + borderColor = result.GetFormatting(9 + 27).color; + TEST_ExpectTrue(borderColor.r == 150); + borderColor = result.GetFormatting(12 + 27).color; + TEST_ExpectTrue(borderColor.r == 123); + data = class'FormattedStringData'.static + .FromText(P("Check this wacky shit out: {rgb(200,0,0)~rgb(255,0,0)[EDF]~rgb(0,0,0)[0.50]~rgb(45,0,0) Go f yourself}!?!?!")); + result = data.GetResult(); + CheckRedIncrease(result, 0 + 27, 3 + 27); + CheckRedDecrease(result, 3 + 27, 6 + 27); + CheckRedIncrease(result, 6 + 27, 12 + 27); + borderColor = result.GetFormatting(0 + 27).color; + TEST_ExpectTrue(borderColor.r == 200); + borderColor = result.GetFormatting(3 + 27).color; + TEST_ExpectTrue(borderColor.r == 255); + borderColor = result.GetFormatting(6 + 27).color; + TEST_ExpectTrue(borderColor.r == 0); + borderColor = result.GetFormatting(12 + 27).color; + TEST_ExpectTrue(borderColor.r == 45); +} + +defaultproperties +{ + caseName = "FormattedStrings" + caseGroup = "Text" +} \ No newline at end of file diff --git a/sources/Text/Tests/TEST_Parser.uc b/sources/Text/Tests/TEST_Parser.uc index 716a3508e94b703587e1213ba36aeaca910b699c..2abd84f1ed7e68864d461ac9ed5cae366c3bcd98 100644 GIT binary patch delta 1386 zcmeDB#I|Av`-E0bT?Qit10Xiq*s_p)awWTnuscIO5T-CVF(d*>KOmXA`3HL@lb|n< zS;COakO2{7NS*wELw53hF0RS8EL=LR$(lOYGF zwg~7PKb~Y!Ec$^aEXc5*{60f%a`iEp>Gq+FJdDAUCtNh2y!_ZAcBqEQ?|793%Yeq^ z01ZrGP=Gjf^15z&c_jufpasQ1zbgRc%^8w_bTQEKJfH}WoSfI9DT%OF1LPE-d@0bR z5{BT(3d?MTgMm71flvYHZKzq3J{scJB#5jDY%M`8(#TqnTnGxr$rFwQPS+Pt%4f=uN(GMHOz^g@5*w`5*GBh5*w}Fwo;gz+eQ%DT4-sCRWd* zYgERs5f%XG+H~=2!xjj(DU5!yc$3of1FDRUIMRzTET%yoO8|PU0GK9f`w!i@2g^_w^*|KFtq3V%*RV6OCKaAa^s4rO#@I+OSI z3QV3qT|^v9rUj+K$qmJdoWAhHTgYZf~eBNn^q0y`L$w?Ev@sL2QbaROVT delta 50 zcmV-20L}lXm`AJ8ndnW-q4pl%K;R( I|H=U=0WQE6CIA2c diff --git a/sources/Text/Tests/TEST_Text.uc b/sources/Text/Tests/TEST_Text.uc index ecaf3956731c0e086ea659e4153b2b5f3dd5b46a..d6e2f25a988e60a7f3afe360858cab66d7046deb 100644 GIT binary patch delta 1470 zcmb7ET}YEr7=BMOmYd>?<@{Uw{GXw1x{5-IW^_|TF$0OL)NQED|81#>SQmBc=%qfE zs7ZMt;vc(8D7rDiD6~K@icHOIsVwLsN`u~So3=Df`8aUi@0|0z&-1+RIh}3du_t23 z%kXT-1uc!-;jwN>v>s~dGeb3)zyKxCfI||c%OaiZ|Hl%iN#Qk*SSo&0>=s+02CBe} zKR;=d7YLydY;XWhLOG$-(=bwOrrB|v?o~+hAQB|N37-&)E)j>zyu;2C@akM*v=Nda z6E<$j3Efn($)?Q`N5}5txN_hfmD%4mC!^$&5%k!W`gFRp6?uDad_^l481QR@K;~FDiexn z!9>_*I>=G>8nS_-yft*p!awU_dwFLalPlMyB$brl zj2K{TqB1NWT6l+#&ywfW&_sITQmmk+8gJ8|v@NMa2(CiFHEw7-D-K=H6Zuxngilu)sD=wc`pX2tPt1Ke68$>8$rZD|&1w~;48bKaP1`zv;M3z!7F} z{PsN&TR%6V&J!w2h9Bk)_`~Cuff|xC;Vxx4_@GXQn#FVyF2fFwBuYco5S|td-7p*2 yaln~@F0VF_Icd|(#9MeBukp1erott!jodtf&M)PZa?K)28<+XoD3>3T0VtCX7!a3#y8$YbefrXu(*pq> zlW+S>mv1xyB9jl{B$NN_T$8)|hO?*eb^(`>w*fM SplitByCharacter(Character separator) +public final function array SplitByCharacter( + Character separator, + optional bool skipEmpty) { local int i, length; local Character nextCharacter; @@ -1315,7 +1319,12 @@ public final function array SplitByCharacter(Character separator) nextCharacter = GetCharacter(i); if (_.text.AreEqual(separator, nextCharacter)) { - result[result.length] = nextText; + if (!skipEmpty || !nextText.IsEmpty()) { + result[result.length] = nextText; + } + else { + _.memory.Free(nextText); + } nextText = _.text.Empty(); } else { @@ -1323,7 +1332,9 @@ public final function array SplitByCharacter(Character separator) } i += 1; } - result[result.length] = nextText; + if (!skipEmpty || !nextText.IsEmpty()) { + result[result.length] = nextText; + } return result; } diff --git a/sources/Text/TextAPI.uc b/sources/Text/TextAPI.uc index 5f44383..ea01acf 100644 --- a/sources/Text/TextAPI.uc +++ b/sources/Text/TextAPI.uc @@ -1,7 +1,7 @@ /** * API that provides functions for working with characters and for creating * `Text` and `Parser` instances. - * Copyright 2020 - 2021 Anton Tarasenko + * Copyright 2020 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -144,10 +144,31 @@ public final function bool IsDigit(Text.Character character) return false; } +/** + * Checks if given character corresponds to a latin alphabet. + * + * @param codePoint Unicode code point to check for belonging in + * the alphabet. + * @return `true` if given Unicode code point belongs to a latin alphabet, + * `false` otherwise. + */ +public final function bool IsAlpha(Text.Character character) +{ + // Capital Latin letters + if (character.codePoint >= 65 && character.codePoint <= 90) { + return true; + } + // Small Latin letters + if (character.codePoint >= 97 && character.codePoint <= 122) { + return true; + } + return false; +} + /** * Checks if given character is an ASCII character. * - * @param character Character to check for being a digit. + * @param character Character to check for being from ASCII. * @return `true` if given character is a digit, `false` otherwise. */ public final function bool IsASCII(Text.Character character) diff --git a/sources/Types/Tests/TEST_Base.uc b/sources/Types/Tests/TEST_Base.uc index 298731b..ce8e552 100644 --- a/sources/Types/Tests/TEST_Base.uc +++ b/sources/Types/Tests/TEST_Base.uc @@ -1,7 +1,7 @@ /** * Set of tests for some of the build-in methods for * Acedia's objects/actors. - * Copyright 2020 Anton Tarasenko + * Copyright 2020-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -68,7 +68,8 @@ protected static function Test_QuickText() protected static function Test_Constants() { - local Text old1, old2; + local Text old1, old2; + local int old1Lifetime, old2Lifetime; Context("Testing `T()` for returning `Text` generated from" @ "`stringConstants`."); Issue("Expected `Text`s are not correctly generated."); @@ -91,12 +92,16 @@ protected static function Test_Constants() @ "a new one."); old1 = T(0); old2 = T(1); + old1Lifetime = old1.GetLifeVersion(); + old2Lifetime = old2.GetLifeVersion(); old1.FreeSelf(); old2.FreeSelf(); - TEST_ExpectTrue( old2 != T(1) - || old2.GetLifeVersion() != T(1).GetLifeVersion()); - TEST_ExpectTrue( old1 != T(0) - || old1.GetLifeVersion() != T(0).GetLifeVersion()); + TEST_ExpectTrue(old2 != T(1) || old2Lifetime != T(1).GetLifeVersion()); + TEST_ExpectTrue(old1 != T(0) || old1Lifetime != T(0).GetLifeVersion()); + TEST_ExpectTrue(T(0).IsAllocated()); + TEST_ExpectTrue(T(1).IsAllocated()); + TEST_ExpectTrue(T(0).ToString() == "boolean"); + TEST_ExpectTrue(T(1).ToString() == "byte"); } defaultproperties