Anton Tarasenko
2 years ago
13 changed files with 1674 additions and 267 deletions
@ -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 |
||||
{ |
||||
} |
@ -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 |
||||
{ |
||||
} |
@ -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 |
||||
{ |
||||
} |
@ -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" |
||||
} |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue