Anton Tarasenko
2 years ago
7 changed files with 1210 additions and 1056 deletions
@ -1,404 +0,0 @@
|
||||
/** |
||||
* Object that is created from *formatted string* (or `Text`) and stores |
||||
* information about formatting used in said string. Was introduced instead of |
||||
* a simple method in `MutableText` to: |
||||
* 1. Allow for reporting errors caused by badly specified colors; |
||||
* 2. Allow for a more complicated case of specifying a color gradient |
||||
* range. |
||||
* Copyright 2022 Anton Tarasenko |
||||
*------------------------------------------------------------------------------ |
||||
* This file is part of Acedia. |
||||
* |
||||
* Acedia is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* Acedia is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class FormattedStringData extends AcediaObject |
||||
dependson(Text) |
||||
dependson(FormattingErrors) |
||||
dependson(FormattingCommandList); |
||||
|
||||
struct FormattingInfo |
||||
{ |
||||
var bool colored; |
||||
var Color plainColor; |
||||
var bool gradient; |
||||
var array<Color> gradientColors; |
||||
var array<float> gradientPoints; |
||||
var int gradientStart; |
||||
var float gradientLength; |
||||
}; |
||||
// Formatted `string` can have an arbitrary level of folded format definitions, |
||||
// this array is used as a stack to keep track of opened formatting blocks |
||||
// when appending formatted `string`. |
||||
var array<FormattingInfo> formattingStack; |
||||
// Keep top element copied into a separate variable for quicker access. |
||||
// Must maintain invariant: if `formattingStack.length > 0` |
||||
// then `formattingStack[formattingStack.length - 1] == formattingStackHead`. |
||||
var FormattingInfo formattingStackHead; |
||||
|
||||
var private FormattingCommandList commands; |
||||
var private MutableText result; |
||||
var private FormattingErrors errors; |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
formattingStack.length = 0; |
||||
_.memory.Free(commands); |
||||
_.memory.Free(errors); |
||||
_.memory.Free(result); |
||||
commands = none; |
||||
errors = none; |
||||
result = none; |
||||
} |
||||
|
||||
public static final function FormattedStringData FromText( |
||||
Text source, |
||||
optional bool doReportErrors) |
||||
{ |
||||
local FormattedStringData newData; |
||||
if (source == none) { |
||||
return none; |
||||
} |
||||
newData = |
||||
FormattedStringData(__().memory.Allocate(class'FormattedStringData')); |
||||
if (doReportErrors) |
||||
{ |
||||
newData.errors = |
||||
FormattingErrors(__().memory.Allocate(class'FormattingErrors')); |
||||
} |
||||
newData.commands = class'FormattingCommandList'.static |
||||
.FromText(source, newData.errors); |
||||
newData.result = __().text.Empty(); |
||||
newData.BuildSelf(); |
||||
__().memory.Free(newData.commands); |
||||
newData.commands = none; |
||||
return newData; |
||||
} |
||||
|
||||
public final function Text GetResult() |
||||
{ |
||||
return result.Copy(); |
||||
} |
||||
|
||||
public final function MutableText GetResultM() |
||||
{ |
||||
return result.MutableCopy(); |
||||
} |
||||
|
||||
public final function FormattingErrors BorrowErrors() |
||||
{ |
||||
return errors; |
||||
} |
||||
|
||||
private final function BuildSelf() |
||||
{ |
||||
local int i, j, nextCharacterIndex; |
||||
local Text.Formatting defaultFormatting; |
||||
local array<Text.Character> nextContents; |
||||
local FormattingCommandList.FormattingCommand nextCommand; |
||||
SetupFormattingStack(defaultFormatting); |
||||
// First element of color stack is special and has no color information; |
||||
// see `BuildFormattingStackCommands()` for details. |
||||
nextCommand = commands.GetCommand(0); |
||||
nextContents = nextCommand.contents; |
||||
result.AppendManyRawCharacters(nextContents); |
||||
nextCharacterIndex = nextContents.length; |
||||
_.memory.Free(nextCommand.tag); |
||||
for (i = 1; i < commands.GetAmount(); i += 1) |
||||
{ |
||||
nextCommand = commands.GetCommand(i); |
||||
if (nextCommand.type == FST_StackPush) { |
||||
PushIntoFormattingStack(nextCommand); |
||||
} |
||||
else if (nextCommand.type == FST_StackPop) { |
||||
PopFormattingStack(); |
||||
} |
||||
else if (nextCommand.type == FST_StackSwap) { |
||||
SwapFormattingStack(nextCommand.charTag); |
||||
} |
||||
nextContents = nextCommand.contents; |
||||
if (IsCurrentFormattingGradient()) |
||||
{ |
||||
for (j = 0; j < nextContents.length; j += 1) |
||||
{ |
||||
result.AppendRawCharacter(nextContents[j], GetFormattingFor(nextCharacterIndex)); |
||||
nextCharacterIndex += 1; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
result.AppendManyRawCharacters(nextContents, GetFormattingFor(nextCharacterIndex)); |
||||
nextCharacterIndex += nextContents.length; |
||||
} |
||||
_.memory.Free(nextCommand.tag); |
||||
} |
||||
} |
||||
|
||||
// Following four functions are to maintain a "color stack" that will |
||||
// remember unclosed colors (new colors are obtained from formatting commands |
||||
// sequence) defined in formatted string, in order. |
||||
// Stack array always contains one element, defined by |
||||
// the `SetupFormattingStack()` call. It corresponds to the default formatting |
||||
// that will be used when we pop all the other elements. |
||||
// It is necessary to deal with possible folded formatting definitions in |
||||
// formatted strings. |
||||
private final function SetupFormattingStack(Text.Formatting defaultFormatting) |
||||
{ |
||||
local FormattingInfo defaultFormattingInfo; |
||||
defaultFormattingInfo.colored = defaultFormatting.isColored; |
||||
defaultFormattingInfo.plainColor = defaultFormatting.color; |
||||
if (formattingStack.length > 0) { |
||||
formattingStack.length = 0; |
||||
} |
||||
formattingStack[0] = defaultFormattingInfo; |
||||
formattingStackHead = defaultFormattingInfo; |
||||
} |
||||
|
||||
private final function bool IsCurrentFormattingGradient() |
||||
{ |
||||
if (formattingStack.length <= 0) { |
||||
return false; |
||||
} |
||||
return formattingStackHead.gradient; |
||||
} |
||||
|
||||
private final function Text.Formatting GetFormattingFor(int index) |
||||
{ |
||||
local Text.Formatting emptyFormatting; |
||||
if (formattingStack.length <= 0) return emptyFormatting; |
||||
if (!formattingStackHead.colored) return emptyFormatting; |
||||
|
||||
return _.text.FormattingFromColor(GetColorFor(index)); |
||||
} |
||||
//FormattedStringData Package.FormattedStringData (Function AcediaCore.FormattedStringData.GetColorFor:00FC) Accessed array 'gradientColors' out of bounds (2/2) |
||||
private final function Color GetColorFor(int index) |
||||
{ |
||||
local int i; |
||||
local float indexPosition, leftPosition, rightPosition; |
||||
local array<float> points; |
||||
local Color leftColor, rightColor, resultColor; |
||||
if (formattingStack.length <= 0) { |
||||
return resultColor; |
||||
} |
||||
if (!formattingStackHead.gradient) { |
||||
return formattingStackHead.plainColor; |
||||
} |
||||
indexPosition = float(index - formattingStackHead.gradientStart) / |
||||
formattingStackHead.gradientLength; |
||||
points = formattingStackHead.gradientPoints; |
||||
for (i = 1; i < points.length; i += 1) |
||||
{ |
||||
if (points[i - 1] <= indexPosition && indexPosition <= points[i]) |
||||
{ |
||||
leftPosition = points[i - 1]; |
||||
rightPosition = points[i]; |
||||
leftColor = formattingStackHead.gradientColors[i - 1]; |
||||
rightColor = formattingStackHead.gradientColors[i]; |
||||
break; |
||||
} |
||||
} |
||||
indexPosition = |
||||
(indexPosition - leftPosition) / (rightPosition - leftPosition); |
||||
resultColor.R = Lerp(indexPosition, leftColor.R, rightColor.R); |
||||
resultColor.G = Lerp(indexPosition, leftColor.G, rightColor.G); |
||||
resultColor.B = Lerp(indexPosition, leftColor.B, rightColor.B); |
||||
resultColor.A = Lerp(indexPosition, leftColor.A, rightColor.A); |
||||
return resultColor; |
||||
} |
||||
|
||||
private final function PushIntoFormattingStack( |
||||
FormattingCommandList.FormattingCommand formattingCommand) |
||||
{ |
||||
formattingStackHead = ParseFormattingInfo(formattingCommand.tag); |
||||
formattingStackHead.gradientStart = formattingCommand.openIndex; |
||||
formattingStackHead.gradientLength = |
||||
float(formattingCommand.closeIndex - formattingCommand.openIndex); |
||||
formattingStack[formattingStack.length] = formattingStackHead; |
||||
} |
||||
|
||||
private final function SwapFormattingStack(Text.Character tagCharacter) |
||||
{ |
||||
local FormattingInfo updatedFormatting; |
||||
if (formattingStack.length > 0) { |
||||
updatedFormatting = formattingStackHead; |
||||
} |
||||
if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.plainColor)) |
||||
{ |
||||
updatedFormatting.colored = true; |
||||
updatedFormatting.gradient = false; |
||||
} |
||||
else { |
||||
Report(FSE_BadShortColorTag, _.text.FromString("^" $ Chr(tagCharacter.codePoint))); |
||||
} |
||||
formattingStackHead = updatedFormatting; |
||||
if (formattingStack.length > 0) { |
||||
formattingStack[formattingStack.length - 1] = updatedFormatting; |
||||
} |
||||
else { |
||||
formattingStack[0] = updatedFormatting; |
||||
} |
||||
} |
||||
|
||||
private final function PopFormattingStack() |
||||
{ |
||||
// Remove the top of the stack |
||||
if (formattingStack.length > 0) { |
||||
formattingStack.length = formattingStack.length - 1; |
||||
} |
||||
// Update the stack head copy |
||||
if (formattingStack.length > 0) { |
||||
formattingStackHead = formattingStack[formattingStack.length - 1]; |
||||
} |
||||
} |
||||
|
||||
private final function FormattingInfo ParseFormattingInfo(Text colorTag) |
||||
{ |
||||
local int i; |
||||
local Parser colorParser; |
||||
local Color nextColor; |
||||
local array<MutableText> specifiedColors; |
||||
local Text.Character tildeCharacter; |
||||
local array<Color> gradientColors; |
||||
local array<float> gradientPoints; |
||||
local FormattingInfo resultInfo; |
||||
if (colorTag.IsEmpty()) |
||||
{ |
||||
Report(FSE_EmptyColorTag); |
||||
return resultInfo; // not colored |
||||
} |
||||
tildeCharacter = _.text.GetCharacter("~"); |
||||
specifiedColors = colorTag.SplitByCharacter(tildeCharacter, true); |
||||
for (i = 0; i < specifiedColors.length; i += 1) |
||||
{ |
||||
colorParser = _.text.Parse(specifiedColors[i]); |
||||
if (_.color.ParseWith(colorParser, nextColor)) |
||||
{ |
||||
colorParser.Confirm(); |
||||
gradientColors[gradientColors.length] = nextColor; |
||||
gradientPoints[gradientPoints.length] = ParsePoint(colorParser); |
||||
} |
||||
else { |
||||
Report(FSE_BadColor, specifiedColors[i]); |
||||
} |
||||
_.memory.Free(colorParser); |
||||
} |
||||
_.memory.FreeMany(specifiedColors); |
||||
gradientPoints = NormalizePoints(gradientPoints); |
||||
resultInfo.colored = (gradientColors.length > 0); |
||||
resultInfo.gradient = (gradientColors.length > 1); |
||||
resultInfo.gradientColors = gradientColors; |
||||
resultInfo.gradientPoints = gradientPoints; |
||||
if (gradientColors.length > 0) { |
||||
resultInfo.plainColor = gradientColors[0]; |
||||
} |
||||
return resultInfo; |
||||
} |
||||
|
||||
private final function float ParsePoint(Parser parser) |
||||
{ |
||||
local float point; |
||||
local Parser.ParserState initialState; |
||||
if (!parser.Ok() || parser.HasFinished()) { |
||||
return -1; |
||||
} |
||||
initialState = parser.GetCurrentState(); |
||||
// [Necessary part] Should starts with "[" |
||||
if (!parser.Match(P("[")).Ok()) |
||||
{ |
||||
Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); |
||||
return -1; |
||||
} |
||||
// [Necessary part] Try parsing number |
||||
parser.MNumber(point).Confirm(); |
||||
if (!parser.Ok()) |
||||
{ |
||||
Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); |
||||
return -1; |
||||
} |
||||
// [Optional part] Check if number is a percentage |
||||
if (parser.Match(P("%")).Ok()) { |
||||
point *= 0.01; |
||||
} |
||||
// This either confirms state of parsing "%" (on success) |
||||
// or reverts to the previous state, just after parsing the number |
||||
// (on failure) |
||||
parser.Confirm(); |
||||
parser.R(); |
||||
// [Necessary part] Have to have closing parenthesis |
||||
if (!parser.HasFinished()) { |
||||
parser.Match(P("]")).Confirm(); |
||||
} |
||||
// Still return `point`, even if there was no closing parenthesis, |
||||
// since that is likely what user wants |
||||
if (!parser.Ok()) { |
||||
Report(FSE_BadGradientPoint, parser.RestoreState(initialState).GetRemainder()); |
||||
} |
||||
return point; |
||||
} |
||||
/*FIRST-POPOPOINTS 0.00 -1.00 -1.00 -1.00 1.00 5 |
||||
PRE-POPOPOINTS 0.00 -1.00 0.00 -1.00 1.00 5 */ |
||||
private final function array<float> NormalizePoints(array<float> points) |
||||
{ |
||||
local int i, j; |
||||
local int negativeSegmentStart, negativeSegmentLength; |
||||
local float lowerBound, upperBound; |
||||
local bool foundNegative; |
||||
if (points.length > 1) |
||||
{ |
||||
points[0] = 0.0; |
||||
points[points.length - 1] = 1.0; |
||||
} |
||||
for (i = 1; i < points.length - 1; i += 1) |
||||
{ |
||||
if (points[i] <= 0 || points[i] > 1 || points[i] <= points[i - 1]) { |
||||
points[i] = -1; |
||||
} |
||||
} |
||||
for (i = 1; i < points.length; i += 1) |
||||
{ |
||||
if (foundNegative && points[i] > 0) |
||||
{ |
||||
upperBound = points[i]; |
||||
for (j = negativeSegmentStart; j < i; j += 1) |
||||
{ |
||||
points[j] = Lerp( float(j - negativeSegmentStart + 1) / float(negativeSegmentLength + 1), |
||||
lowerBound, upperBound); |
||||
} |
||||
negativeSegmentLength = 0; |
||||
} |
||||
if (!foundNegative && points[i] < 0) |
||||
{ |
||||
lowerBound = points[i - 1]; |
||||
negativeSegmentStart = i; |
||||
} |
||||
foundNegative = (points[i] < 0); |
||||
if (foundNegative) { |
||||
negativeSegmentLength += 1; |
||||
} |
||||
} |
||||
return points; |
||||
} |
||||
|
||||
public final function Report( |
||||
FormattingErrors.FormattedDataErrorType type, |
||||
optional Text cause) |
||||
{ |
||||
if (errors == none) { |
||||
return; |
||||
} |
||||
errors.Report(type, cause); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -1,296 +0,0 @@
|
||||
/** |
||||
* Formatted string can be thought of as a string with a sequence of |
||||
* formatting-changing commands specified within it (either by opening new |
||||
* formatting block, swapping to color with "^" or by closing it and reverting |
||||
* to the previous one). |
||||
* This objects allows to directly access these commands. |
||||
* Copyright 2022 Anton Tarasenko |
||||
*------------------------------------------------------------------------------ |
||||
* This file is part of Acedia. |
||||
* |
||||
* Acedia is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* Acedia is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class FormattingCommandList extends AcediaObject |
||||
dependson(Text); |
||||
|
||||
enum FormattingCommandType |
||||
{ |
||||
// Push more data onto formatted stack |
||||
FST_StackPush, |
||||
// Pop data from formatted stack |
||||
FST_StackPop, |
||||
// Swap the top value on the formatting stack |
||||
FST_StackSwap |
||||
}; |
||||
|
||||
// Formatted `string` is separated into several (possibly nested) parts, |
||||
// each with its own formatting. These can be easily handled with a formatting |
||||
// stack: |
||||
// * Each time a new section opens ("{<color_tag> ") we put another, |
||||
// current formatting on top of the stack; |
||||
// * Each time a section closes ("}") we pop the stack, returning to |
||||
// a previous formatting. |
||||
// * In a special case of "^" color swap that is supposed to last until |
||||
// current block closes we simply swap the color of the formatting on |
||||
// top of the stack. |
||||
struct FormattingCommand |
||||
{ |
||||
// Only defined for `FST_StackPush` commands that correspond to section |
||||
// openers ("{<color_tag> "). |
||||
// Indices of first and last character belonging to block it opens. |
||||
var int openIndex; |
||||
var int closeIndex; |
||||
// Did this block start by opening or closing formatted part? |
||||
// Ignored for the very first block without any formatting. |
||||
var FormattingCommandType type; |
||||
// Full text inside the block, without any formatting |
||||
var array<Text.Character> contents; |
||||
// Formatting tag for the next block |
||||
// (only used for `FST_StackPush` command type) |
||||
var MutableText tag; |
||||
// Formatting character for the "^"-type tag |
||||
// (only used for `FST_StackSwap` command type) |
||||
var Text.Character charTag; |
||||
}; |
||||
// Appending formatted `string` into the `MutableText` first requires its |
||||
// transformation into series of `FormattingCommand` and then their |
||||
// execution to assemble the `MutableText`. |
||||
// First element of `commandList` is special and is used solely as |
||||
// a container for unformatted data. It should not be used to execute |
||||
// formatting stack commands. |
||||
// This variable contains intermediary data. |
||||
var private array<FormattingCommand> commandList; |
||||
|
||||
// Stack that keeps track of which (by index inside `commandList`) command |
||||
// opened section we are currently parsing. This is needed to record positions |
||||
// at which each block is opened and closed. |
||||
var private array<int> pushCommandIndicesStack; |
||||
// Store contents for the next command here, because appending array in |
||||
// the struct is expensive |
||||
var private array<Text.Character> currentContents; |
||||
// `Parser` used to break input formatted string into commands, only used |
||||
// during building this object (inside `BuildSelf()` method). |
||||
var private Parser parser; |
||||
// `FormattingErrors` object used to reports errors during building process. |
||||
// It is "borrowed" - meaning that we do not really own it and should not |
||||
// deallocate it. Only set as a field for convenience. |
||||
var private FormattingErrors borrowedErrors; |
||||
|
||||
const CODEPOINT_ESCAPE = 27; // ASCII escape code |
||||
const CODEPOINT_OPEN_FORMAT = 123; // '{' |
||||
const CODEPOINT_CLOSE_FORMAT = 125; // '}' |
||||
const CODEPOINT_FORMAT_ESCAPE = 38; // '&' |
||||
const CODEPOINT_ACCENT = 94; // '^' |
||||
const CODEPOINT_TILDE = 126; // '~' |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
local int i; |
||||
_.memory.Free(parser); |
||||
parser = none; |
||||
borrowedErrors = none; |
||||
if (currentContents.length > 0) { |
||||
currentContents.length = 0; |
||||
} |
||||
if (pushCommandIndicesStack.length > 0) { |
||||
pushCommandIndicesStack.length = 0; |
||||
} |
||||
for (i = 0; i < commandList.length; i += 1) { |
||||
_.memory.Free(commandList[i].tag); |
||||
} |
||||
if (commandList.length > 0) { |
||||
commandList.length = 0; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Create `FormattingCommandList` based on given `Text`. |
||||
* |
||||
* @param input `Text` that should be treated as "formatted" and |
||||
* to be broken into formatting commands. |
||||
* @param errorsReporter If specified, will be used to report errors |
||||
* (can only report `FSE_UnmatchedClosingBrackets`). |
||||
* @return New `FormattingCommandList` instance that allows us to have direct |
||||
* access to formatting commands. |
||||
*/ |
||||
public final static function FormattingCommandList FromText( |
||||
Text input, |
||||
optional FormattingErrors errorsReporter) |
||||
{ |
||||
local FormattingCommandList newList; |
||||
newList = FormattingCommandList( |
||||
__().memory.Allocate(class'FormattingCommandList')); |
||||
newList.parser = __().text.Parse(input); |
||||
newList.borrowedErrors = errorsReporter; |
||||
newList.BuildSelf(); |
||||
__().memory.Free(newList.parser); |
||||
newList.parser = none; |
||||
newList.borrowedErrors = none; |
||||
return newList; |
||||
} |
||||
|
||||
/** |
||||
* Returns command with index `commandIndex`. |
||||
* |
||||
* @param commandIndex Index of the command to return. |
||||
* Must be non-negative (`>= 0`) and less than `GetAmount()`. |
||||
* @return Command with index `commandIndex`. |
||||
* If given `commandIndex` is out of bounds - returns invalid command. |
||||
* `tag` field is guaranteed to be non-`none` and should be deallocated. |
||||
*/ |
||||
public final function FormattingCommand GetCommand(int commandIndex) |
||||
{ |
||||
local MutableText resultTag; |
||||
local FormattingCommand result; |
||||
if (commandIndex < 0) return result; |
||||
if (commandIndex >= commandList.length) return result; |
||||
|
||||
result = commandList[commandIndex]; |
||||
resultTag = result.tag; |
||||
if (resultTag != none) { |
||||
result.tag = resultTag.MutableCopy(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Returns amount of commands inside caller `FormattingCommandList`. |
||||
* |
||||
* @return Amount of commands inside caller `FormattingCommandList`. |
||||
*/ |
||||
public final function int GetAmount() |
||||
{ |
||||
return commandList.length; |
||||
} |
||||
|
||||
// Method that turns `parser` into proper `FormattingCommandList` object. |
||||
private final function BuildSelf() |
||||
{ |
||||
//local int i; |
||||
local int characterCounter; |
||||
local Text.Character nextCharacter; |
||||
local FormattingCommand nextCommand; |
||||
while (!parser.HasFinished()) |
||||
{ |
||||
parser.MCharacter(nextCharacter); |
||||
// New command by "{<formatting_info>" |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) |
||||
{ |
||||
nextCommand = AddCommand(nextCommand, FST_StackPush, characterCounter); |
||||
parser.MUntil(nextCommand.tag,, true) |
||||
.MCharacter(nextCommand.charTag); // Simply to skip a char |
||||
continue; |
||||
} |
||||
// New command by "}" |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) |
||||
{ |
||||
nextCommand = AddCommand(nextCommand, FST_StackPop, characterCounter); |
||||
continue; |
||||
} |
||||
// New command by "^" |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) |
||||
{ |
||||
nextCommand = AddCommand(nextCommand, FST_StackSwap, characterCounter); |
||||
parser.MCharacter(nextCommand.charTag); |
||||
if (!parser.Ok()) { |
||||
break; |
||||
} |
||||
continue; |
||||
} |
||||
// Escaped sequence |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { |
||||
parser.MCharacter(nextCharacter); |
||||
} |
||||
if (!parser.Ok()) { |
||||
break; |
||||
} |
||||
currentContents[currentContents.length] = nextCharacter; |
||||
characterCounter += 1; |
||||
} |
||||
// Only put in empty command if there is nothing else. |
||||
if (currentContents.length > 0 || commandList.length == 0) |
||||
{ |
||||
nextCommand.contents = currentContents; |
||||
commandList[commandList.length] = nextCommand; |
||||
} |
||||
/*for (i = 0; i < commandList.length; i += 1) |
||||
{ |
||||
Log(">>>COMMAND LIST FOR" @ i $ "<<<"); |
||||
Log("OPEN/CLOSE:" @ commandList[i].openIndex @ "/" @ commandList[i].closeIndex); |
||||
Log("TYPE:" @ commandList[i].type); |
||||
Log("CONTETS LENGTH:" @ commandList[i].contents.length); |
||||
if (commandList[i].tag != none) { |
||||
Log("TAG:" @ commandList[i].tag.ToString()); |
||||
} |
||||
else { |
||||
Log("TAG: NONE"); |
||||
} |
||||
}*/ |
||||
} |
||||
|
||||
// Helper method for a quick creation of a new `FormattingCommand` |
||||
private final function FormattingCommand AddCommand( |
||||
FormattingCommand nextCommand, |
||||
FormattingCommandType newStackCommandType, |
||||
optional int currentCharacterIndex) |
||||
{ |
||||
local int lastPushIndex; |
||||
local FormattingCommand newCommand; |
||||
nextCommand.contents = currentContents; |
||||
if (currentContents.length > 0) { |
||||
currentContents.length = 0; |
||||
} |
||||
commandList[commandList.length] = nextCommand; |
||||
if (newStackCommandType == FST_StackPop) |
||||
{ |
||||
lastPushIndex = PopIndex(); |
||||
if (lastPushIndex >= 0) { |
||||
// BLABLA |
||||
commandList[lastPushIndex].closeIndex = currentCharacterIndex - 1; |
||||
} |
||||
else if (borrowedErrors != none) { |
||||
borrowedErrors.Report(FSE_UnmatchedClosingBrackets); |
||||
} |
||||
} |
||||
newCommand.type = newStackCommandType; |
||||
if (newStackCommandType == FST_StackPush) |
||||
{ |
||||
newCommand.openIndex = currentCharacterIndex; |
||||
newCommand.closeIndex = -1; |
||||
// BLABLA |
||||
PushIndex(commandList.length); |
||||
} |
||||
return newCommand; |
||||
} |
||||
|
||||
private final function PushIndex(int index) |
||||
{ |
||||
pushCommandIndicesStack[pushCommandIndicesStack.length] = |
||||
commandList.length; |
||||
} |
||||
|
||||
private final function int PopIndex() |
||||
{ |
||||
local int result; |
||||
if (pushCommandIndicesStack.length <= 0) { |
||||
return -1; |
||||
} |
||||
result = pushCommandIndicesStack[pushCommandIndicesStack.length - 1]; |
||||
pushCommandIndicesStack.length = pushCommandIndicesStack.length - 1; |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,316 @@
|
||||
/** |
||||
* Formatted string can be thought of as a string with a sequence of |
||||
* formatting-changing commands specified within it, along with raw contents |
||||
* to be pasted before performing next command (for more information about this |
||||
* see `FormattingStringParser`). This is a class for an accessor object |
||||
* that can return these individual commands based on the given `Text`/`string` |
||||
* (alongside with the construction code that determines these commands). |
||||
* This objects allows to directly access these commands. |
||||
* Copyright 2022 Anton Tarasenko |
||||
*------------------------------------------------------------------------------ |
||||
* This file is part of Acedia. |
||||
* |
||||
* Acedia is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* Acedia is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class FormattingCommandsSequence extends AcediaObject |
||||
dependson(Text); |
||||
|
||||
enum FormattingCommandType |
||||
{ |
||||
// Push more new formatting onto the stack. Corresponds to "{<color_tag> ". |
||||
FST_StackPush, |
||||
// Pop formatting from the stack. Corresponds to "}". |
||||
FST_StackPop, |
||||
// Swap the top value on the formatting stack for a different formatting |
||||
// (pushes new one, if the stack is empty). Corresponds to "^<color_char>". |
||||
FST_StackSwap |
||||
}; |
||||
|
||||
/** |
||||
* Represents formatting command + contents, alongside some additional |
||||
* meta information, necessary for `FormattingStringParser`. |
||||
*/ |
||||
struct FormattingCommand |
||||
{ |
||||
var FormattingCommandType type; |
||||
var array<Text.Character> contents; |
||||
|
||||
// Formatting character for the "^"-type tag |
||||
// This parameter is only used for `FST_StackSwap` command type. |
||||
var Text.Character charTag; |
||||
|
||||
// Rest of the parameters are only used for `FST_StackPush` |
||||
// command type. |
||||
// These commands correspond to section openers ("{<color_tag> "): |
||||
// such openings define a *formatting block* between itself and matching |
||||
// closing curly braces "}". |
||||
// Meta information about these blocks is necessary for |
||||
// `FormattingStringParser`. |
||||
|
||||
// Formatting tag for the next block - "<color_tag>" from "{<color_tag> ". |
||||
var MutableText tag; |
||||
// When formatting block for this command started and ended - |
||||
// necessary for gradient coloring. |
||||
// `closeIndex` should be equal to `-1` if it is not defined. |
||||
var int openIndex; |
||||
var int closeIndex; |
||||
}; |
||||
// All the commands we got from formatted string |
||||
var private array<FormattingCommand> commandSequence; |
||||
// Store contents for the next command here, because constantly appending array |
||||
// inside the struct (`FormattingCommand` here) is expensive. |
||||
var private array<Text.Character> currentContents; |
||||
// `Parser` used to break input formatted string into commands. |
||||
// It is only used during building this object (inside `BuildSelf()` method). |
||||
var private Parser parser; |
||||
// How many non-formatting defining characters we have parsed. |
||||
// That is characters that are actually meant to be displayed to the user |
||||
// and not the part of formatting definitions (e.g. "{$red", "}" or "^r"; |
||||
// "&{", "&&" or "&^" are also resolved into a single displayed character). |
||||
// `Parser`'s `GetParsedLength()` method is unusable here, since it |
||||
// reports all parsed characters. |
||||
var private int characterCounter; |
||||
// Command we are currently building. |
||||
// Making it a field makes code simpler and lets us avoid passing |
||||
// `FormattingCommand` struct between functions. |
||||
var private FormattingCommand currentCommand; |
||||
// `FormattingErrorsReport` we are given to report errors to. |
||||
// It is considered "borrowed": we do not really own it and will not |
||||
// deallocate it. |
||||
// Since, similar to `parser` field, it is only used during building this |
||||
// object - there is no danger of it being deallocated while we are storing |
||||
// this reference. |
||||
// Only set as a field for convenience, to avoid passing it as a parameter |
||||
// between methods during parsing. |
||||
var private FormattingErrorsReport borrowedErrors; |
||||
|
||||
// Stack that keeps track of which (by index inside `commandSequence`) command |
||||
// opened section we are currently parsing. This is needed to record positions |
||||
// at which each block is opened and closed. |
||||
|
||||
// It is easy to record opening indices for each formatting block by |
||||
// recording how many characters we have already processed before encountering |
||||
// opening statement "{<color_tag> ". But to record closing indices we need to |
||||
// correspond correct opener with correct closer ("}"). |
||||
// We accomplish that by keeping track of all formatted blocks opened |
||||
// at the current moment during parsing in a stack and popping the top value |
||||
// upon reaching "}". |
||||
// We identify each formatting block by recording index of corresponding |
||||
// `FormattingCommand` inside `commandSequence` array. This makes setting |
||||
// appropriate `closeIndex` simple. |
||||
var private array<int> pushCommandIndicesStack; |
||||
|
||||
const CODEPOINT_OPEN_FORMAT = 123; // '{' |
||||
const CODEPOINT_CLOSE_FORMAT = 125; // '}' |
||||
const CODEPOINT_FORMAT_ESCAPE = 38; // '&' |
||||
const CODEPOINT_ACCENT = 94; // '^' |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
local int i; |
||||
for (i = 0; i < commandSequence.length; i += 1) { |
||||
_.memory.Free(commandSequence[i].tag); |
||||
} |
||||
pushCommandIndicesStack.length = 0; |
||||
currentContents.length = 0; |
||||
commandSequence.length = 0; |
||||
characterCounter = 0; |
||||
// These fields should not be set at this point, but clean them up |
||||
// just in case |
||||
_.memory.Free(parser); |
||||
parser = none; |
||||
borrowedErrors = none; |
||||
} |
||||
|
||||
/** |
||||
* Create `FormattingCommandsSequence` based on the given `Text`. |
||||
* |
||||
* There is not separate method for `string`, since we would require reading it |
||||
* into a `Text` as a *plain string* first anyway and, as this class is |
||||
* technical/internal, no convenience methods are needed. |
||||
* |
||||
* @param input `Text` that should be treated as "formatted" and |
||||
* to be broken into formatting commands. |
||||
* @param errorsReporter If specified, will be used to report errors detected |
||||
* during construction of `FormattingCommandsSequence` |
||||
* (can only report `FSE_UnmatchedClosingBrackets`). |
||||
* @return New `FormattingCommandsSequence` instance that allows us to have |
||||
* direct access to formatting commands defined in `input`. |
||||
*/ |
||||
public final static function FormattingCommandsSequence FromText( |
||||
Text input, |
||||
optional FormattingErrorsReport errorsReporter) |
||||
{ |
||||
local FormattingCommandsSequence newSequence; |
||||
newSequence = FormattingCommandsSequence( |
||||
__().memory.Allocate(class'FormattingCommandsSequence')); |
||||
// Setup variables |
||||
newSequence.parser = __().text.Parse(input); |
||||
newSequence.borrowedErrors = errorsReporter; |
||||
// Parse |
||||
newSequence.BuildSelf(); |
||||
// Clean up |
||||
__().memory.Free(newSequence.parser); |
||||
newSequence.parser = none; |
||||
newSequence.borrowedErrors = none; |
||||
return newSequence; |
||||
} |
||||
|
||||
/** |
||||
* Amount of commands to reconstruct formatted string caller |
||||
* `FormattingCommandsSequence` was created from. |
||||
* |
||||
* @return Amount of commands inside caller `FormattingCommandsSequence`. |
||||
*/ |
||||
public final function int GetAmount() |
||||
{ |
||||
return commandSequence.length; |
||||
} |
||||
|
||||
/** |
||||
* Returns command with index `commandIndex`. Indexation starts from `0`. |
||||
* |
||||
* @param commandIndex Index of the command to return. |
||||
* Must be non-negative (`>= 0`) and less than `GetAmount()`. |
||||
* @return Command with index `commandIndex`. |
||||
* If given `commandIndex` is out of bounds - returns invalid command. |
||||
* `tag` field is guaranteed to be non-`none` for commands of type |
||||
* `FST_StackPush` and should be deallocated, as per usual rules. |
||||
*/ |
||||
public final function FormattingCommand GetCommand(int commandIndex) |
||||
{ |
||||
local MutableText resultTag; |
||||
local FormattingCommand result; |
||||
if (commandIndex < 0) return result; |
||||
if (commandIndex >= commandSequence.length) return result; |
||||
|
||||
result = commandSequence[commandIndex]; |
||||
resultTag = result.tag; |
||||
if (resultTag != none) { |
||||
result.tag = resultTag.MutableCopy(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private final function BuildSelf() |
||||
{ |
||||
local Text.Character nextCharacter; |
||||
while (!parser.HasFinished()) |
||||
{ |
||||
parser.MCharacter(nextCharacter); |
||||
// New command by "{<formatting_info> " |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) |
||||
{ |
||||
AddCommand(FST_StackPush); |
||||
parser |
||||
.MUntil(currentCommand.tag,, true) |
||||
.MCharacter(currentCommand.charTag); // Simply to skip a char |
||||
continue; |
||||
} |
||||
// New command by "}" |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) |
||||
{ |
||||
AddCommand(FST_StackPop); |
||||
continue; |
||||
} |
||||
// New command by "^" |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) |
||||
{ |
||||
AddCommand(FST_StackSwap); |
||||
parser.MCharacter(currentCommand.charTag); |
||||
if (!parser.Ok()) { |
||||
break; |
||||
} |
||||
continue; |
||||
} |
||||
// Escaped sequence |
||||
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { |
||||
parser.MCharacter(nextCharacter); |
||||
} |
||||
if (!parser.Ok()) { |
||||
break; |
||||
} |
||||
currentContents[currentContents.length] = nextCharacter; |
||||
characterCounter += 1; |
||||
} |
||||
// Only put in empty command if there is nothing else |
||||
if (currentContents.length > 0 || commandSequence.length == 0) |
||||
{ |
||||
currentCommand.contents = currentContents; |
||||
commandSequence[commandSequence.length] = currentCommand; |
||||
} |
||||
// We no longer use `currentCommand` and have transferred ownership over |
||||
// `currentCommand.tag` to the `commandSequence`, so better forget about it |
||||
// to avoid messing up. |
||||
currentCommand.tag = none; |
||||
} |
||||
|
||||
// Helper method for a adding `currentCommand` to the command sequence and |
||||
// quick creation of a new `FormattingCommand` in its place |
||||
private final function AddCommand(FormattingCommandType newStackCommandType) |
||||
{ |
||||
local int lastPushIndex; |
||||
local int lastCharacterIndex; |
||||
local FormattingCommand newCommand; |
||||
currentCommand.contents = currentContents; |
||||
currentContents.length = 0; |
||||
commandSequence[commandSequence.length] = currentCommand; |
||||
// Last (so far) character index in a string equals total amount of |
||||
// parsed characters minus one |
||||
lastCharacterIndex = characterCounter - 1; |
||||
if (newStackCommandType == FST_StackPop) |
||||
{ |
||||
lastPushIndex = PopIndex(); |
||||
if (lastPushIndex >= 0) { |
||||
commandSequence[lastPushIndex].closeIndex = lastCharacterIndex; |
||||
} |
||||
else if (borrowedErrors != none) { |
||||
borrowedErrors.Report(FSE_UnmatchedClosingBrackets); |
||||
} |
||||
} |
||||
newCommand.type = newStackCommandType; |
||||
if (newStackCommandType == FST_StackPush) |
||||
{ |
||||
// Formatting should be applied to the next character, |
||||
// not the currently last added one |
||||
newCommand.openIndex = lastCharacterIndex + 1; |
||||
newCommand.closeIndex = -1; |
||||
// `FormattingCommand` that new formatting block corresponds to |
||||
// is not added yet, but it is guaranteed to be added next, |
||||
// we know its future index |
||||
PushIndex(commandSequence.length); |
||||
} |
||||
currentCommand = newCommand; |
||||
} |
||||
|
||||
private final function int PopIndex() |
||||
{ |
||||
local int result; |
||||
if (pushCommandIndicesStack.length <= 0) { |
||||
return -1; |
||||
} |
||||
result = pushCommandIndicesStack[pushCommandIndicesStack.length - 1]; |
||||
pushCommandIndicesStack.length = pushCommandIndicesStack.length - 1; |
||||
return result; |
||||
} |
||||
|
||||
private final function PushIndex(int index) |
||||
{ |
||||
pushCommandIndicesStack[pushCommandIndicesStack.length] = |
||||
commandSequence.length; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,548 @@
|
||||
/** |
||||
* A simple parser with a single public method for parsing formatted strings. |
||||
* Was introduced instead of a simple method in `MutableText` to: |
||||
* 1. Allow for reporting errors caused by badly specified colors; |
||||
* 2. Allow for a more complicated case of specifying a color gradient |
||||
* range. |
||||
* Copyright 2022 Anton Tarasenko |
||||
*------------------------------------------------------------------------------ |
||||
* This file is part of Acedia. |
||||
* |
||||
* Acedia is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* Acedia is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class FormattingStringParser extends AcediaObject |
||||
dependson(Text) |
||||
dependson(FormattingErrorsReport) |
||||
dependson(FormattingCommandsSequence); |
||||
|
||||
/** |
||||
* # Usage |
||||
* |
||||
* Public interface of this parser consists of a single static method |
||||
* `ParseFormatted()` that temporarily creates (and auto deallocates before |
||||
* method has returned) instance of its own class to store the state necessary |
||||
* during parsing. |
||||
* |
||||
* # Implementation |
||||
* |
||||
* ## Formatting commands |
||||
* |
||||
* The algorithm looks at formatting block "{<color_tag> ...}" as a set |
||||
* of two operations: turn on a certain formatting ("{<color_tag>") and |
||||
* turn it off ("}"). Since blocks can be folded into each other, as we parse |
||||
* the string we put opened ones onto the stack, closing them upon |
||||
* encountering "}". Short color tags "^<color_character>" are handled by |
||||
* switching formatting within the current block. |
||||
* Overall this leads us to transforming markdown of formatted string into |
||||
* sequence of three operations: |
||||
* 1. Put formatting onto the formatting stack (and add following |
||||
* contents); |
||||
* 2. Pop formatting off the formatting stack (and add following contents); |
||||
* 3. Swap formatting on top of the formatting stack (and add following |
||||
* contents). |
||||
* Transforming formatted string in such a sequence is moved out from this |
||||
* class into auxiliary `FormattingCommandsSequence`, since it task is |
||||
* logically separated from the one that comes next... |
||||
* |
||||
* ## Building `MutableText` |
||||
* |
||||
* Once we have broken formatted string down into sequence of formatted |
||||
* commands, we only need to go through them, command-by-command, appending |
||||
* their contents to the resulting `MutableText` with formatting, specified by |
||||
* the command. |
||||
* The only somewhat complicated part here are formatted blocks with |
||||
* specified gradient coloring |
||||
* ("<color_1>:<color_2>:<color_3>[<point>]:<color_4>"). For that we make use |
||||
* of the information about starting and ending indices of gradient formatted |
||||
* block, given by `FormattingCommandsSequence`: |
||||
* * We correct/uniformly place missing points where each intermediate |
||||
* color must be at 100% on the segment [0; 1], where 0 represents |
||||
* start of the formatting block and 1 its end; |
||||
* * Then for each character we determine between which color it lies and |
||||
* how far from each of them (again on the scale from 0 to 1, |
||||
* where `0` is left color and `1` is right color); |
||||
* * Finally we use linear interpolation between selected pair of colors to |
||||
* determine appropriate formatting. |
||||
*/ |
||||
|
||||
/** |
||||
* Element of the formatting stack, that completely defines formatting block. |
||||
*/ |
||||
struct FormattingInfo |
||||
{ |
||||
// Is segment even colored? |
||||
var bool colored; |
||||
// Does it use color gradient? |
||||
var bool gradient; |
||||
// Color of the segment, only used when `gradient` equals `true` |
||||
var Color plainColor; |
||||
// All the colors for gradient inside the segment, only used when |
||||
// `gradient` equals `false` |
||||
var array<Color> gradientColors; |
||||
// Points (from 0 to 1) at which each `gradientColors` with the same index |
||||
// should be at its 100% |
||||
var array<float> gradientPoints; |
||||
// To decide how to color each character we need to know position of |
||||
// the segment, it is convenient for us to store it as a starting point |
||||
// and length. |
||||
// Length is stored as `float` because it will mostly be used as |
||||
// a divisor of some values and we need a `float` result. |
||||
var int gradientStart; |
||||
var float gradientLength; |
||||
}; |
||||
// Formatted `string` can have an arbitrary level of folded format definitions, |
||||
// this array is used as a stack to keep track of opened formatting blocks |
||||
// when appending formatted `string`. |
||||
var private array<FormattingInfo> formattingStack; |
||||
// Keep top element copied into a separate variable for quicker access. |
||||
// Must maintain invariant: if `formattingStack.length > 0` |
||||
// then `formattingStack[formattingStack.length - 1] == formattingStackHead`. |
||||
var private FormattingInfo formattingStackHead; |
||||
// For calculating gradient we need to know what character we are |
||||
// currently adding. |
||||
var private int nextCharacterIndex; |
||||
// `FormattingStringParser` itself only performs "stage 2" of the algorithm, |
||||
// while "stage 1" (converting formatted string into a sequence of commands is |
||||
// done by this object). |
||||
var private FormattingCommandsSequence commandSequence; |
||||
// Text we are appending formatted string to |
||||
var private MutableText borrowedTarget; |
||||
var private FormattingErrorsReport borrowedErrors; |
||||
|
||||
// Keep this as an easy access to separator of gradient colors ':' |
||||
var private Text.Character separatorCharacter; |
||||
|
||||
var private const int TOPENING_BRACKET, TCLOSING_BRACKET, TPERCENT; |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
separatorCharacter = _.text.GetCharacter(":"); |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
formattingStack.length = 0; |
||||
_.memory.Free(commandSequence); // the only object we have owned |
||||
commandSequence = none; |
||||
borrowedTarget = none; |
||||
borrowedErrors = none; |
||||
} |
||||
|
||||
/** |
||||
* Parses formatted string given by the `source`. |
||||
* |
||||
* As a result of parsing can either append it to given `MutableText` or |
||||
* report any errors in its formatting. |
||||
* |
||||
* @param source `Text` to parse as a formatted string. |
||||
* @param target Method will append result of parsing `source` into |
||||
* this parameter. Does nothing if it is equal to `none`. |
||||
* @param doReportErrors Set this to `true` if you want parsing errors to be |
||||
* reported in the return value and `false` otherwise. |
||||
* @return Array of formatting errors in the given `source` formatted string, |
||||
* each represented by `FormattedStringError` struct. |
||||
* Errors are only generated if `doReportErrors` is equals to `true`. |
||||
* If `doReportErrors` is `false`, then returned value is guaranteed to be |
||||
* an empty array. |
||||
* Each `FormattedStringError` item in array has either: |
||||
* * non-`none` `cause` field or; |
||||
* * strictly positive `count > 0` field. |
||||
* But never both. |
||||
* `count` field is always guaranteed to be non-negative. |
||||
* WARNING: `FormattedStringError` struct may contain `Text` objects that |
||||
* should be deallocated, as per usual rules. |
||||
*/ |
||||
public static final function array<FormattingErrorsReport.FormattedStringError> |
||||
ParseFormatted( |
||||
Text source, |
||||
optional MutableText target, |
||||
optional bool doReportErrors) |
||||
{ |
||||
local FormattingErrorsReport newErrorsReport; |
||||
local FormattingStringParser newFormattingParser; |
||||
local array<FormattingErrorsReport.FormattedStringError> resultErrors; |
||||
if (source == none) return resultErrors; |
||||
if (target == none && !doReportErrors) return resultErrors; |
||||
|
||||
// Setup formatting parser |
||||
newFormattingParser = FormattingStringParser(__().memory |
||||
.Allocate(class'FormattingStringParser')); |
||||
if (doReportErrors) |
||||
{ |
||||
newErrorsReport = FormattingErrorsReport(__().memory |
||||
.Allocate(class'FormattingErrorsReport')); |
||||
newFormattingParser.borrowedErrors = newErrorsReport; |
||||
} |
||||
newFormattingParser.commandSequence = |
||||
class'FormattingCommandsSequence'.static |
||||
.FromText(source, newErrorsReport); |
||||
newFormattingParser.borrowedTarget = target; |
||||
// Do it and release resources |
||||
newFormattingParser.DoAppend(); |
||||
// We have only set these fields for access convenience and we |
||||
// neither own `target` that will contain appended formatted string, |
||||
// nor errors report that user requires, so release them right after use |
||||
newFormattingParser.borrowedTarget = none; |
||||
newFormattingParser.borrowedErrors = none; |
||||
__().memory.Free(newFormattingParser); |
||||
if (newErrorsReport != none) |
||||
{ |
||||
resultErrors = newErrorsReport.GetErrors(); |
||||
__().memory.Free(newErrorsReport); |
||||
} |
||||
return resultErrors; |
||||
} |
||||
|
||||
private final function DoAppend() |
||||
{ |
||||
local int i; |
||||
local Text.Formatting emptyFormatting; |
||||
local FormattingCommandsSequence.FormattingCommand nextCommand; |
||||
SetupFormattingStack(emptyFormatting); |
||||
// First element of color stack is special and has no color information; |
||||
// see `BuildFormattingStackCommands()` for details. |
||||
nextCommand = commandSequence.GetCommand(0); |
||||
// First block is always not formatted |
||||
if (borrowedTarget != none) { |
||||
borrowedTarget.AppendManyRawCharacters(nextCommand.contents); |
||||
} |
||||
nextCharacterIndex = nextCommand.contents.length; |
||||
_.memory.Free(nextCommand.tag); |
||||
for (i = 1; i < commandSequence.GetAmount(); i += 1) |
||||
{ |
||||
nextCommand = commandSequence.GetCommand(i); |
||||
if (nextCommand.type == FST_StackPush) { |
||||
PushIntoFormattingStack(nextCommand); |
||||
} |
||||
else if (nextCommand.type == FST_StackPop) { |
||||
PopFormattingStack(); |
||||
} |
||||
else if (nextCommand.type == FST_StackSwap) { |
||||
SwapFormattingStack(nextCommand.charTag); |
||||
} |
||||
_.memory.Free(nextCommand.tag); |
||||
if (borrowedTarget != none) { |
||||
AppendToTarget(nextCommand.contents); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Auxiliary method for appending `contents` character with an appropriate |
||||
// formatting and parser's state modification. |
||||
private final function AppendToTarget(array<Text.Character> contents) |
||||
{ |
||||
local int i; |
||||
if (!IsCurrentFormattingGradient()) |
||||
{ |
||||
borrowedTarget.AppendManyRawCharacters( |
||||
contents, |
||||
GetFormattingFor(nextCharacterIndex)); |
||||
nextCharacterIndex += contents.length; |
||||
return; |
||||
} |
||||
for (i = 0; i < contents.length; i += 1) |
||||
{ |
||||
borrowedTarget.AppendRawCharacter( |
||||
contents[i], |
||||
GetFormattingFor(nextCharacterIndex)); |
||||
nextCharacterIndex += 1; |
||||
} |
||||
} |
||||
|
||||
private final function Report( |
||||
FormattingErrorsReport.FormattedStringErrorType type, |
||||
optional Text cause) |
||||
{ |
||||
if (borrowedErrors == none) { |
||||
return; |
||||
} |
||||
borrowedErrors.Report(type, cause); |
||||
} |
||||
|
||||
private final function bool IsCurrentFormattingGradient() |
||||
{ |
||||
if (formattingStack.length <= 0) { |
||||
return false; |
||||
} |
||||
return formattingStackHead.gradient; |
||||
} |
||||
|
||||
private final function Text.Formatting GetFormattingFor(int index) |
||||
{ |
||||
local Text.Formatting emptyFormatting; |
||||
if (formattingStack.length <= 0) return emptyFormatting; |
||||
if (!formattingStackHead.colored) return emptyFormatting; |
||||
|
||||
return _.text.FormattingFromColor(GetColorFor(index)); |
||||
} |
||||
|
||||
private final function Color GetColorFor(int index) |
||||
{ |
||||
local int i; |
||||
local float indexPosition, leftPosition, rightPosition; |
||||
local array<float> points; |
||||
local Color leftColor, rightColor, targetColor; |
||||
if (formattingStack.length <= 0) return targetColor; |
||||
if (!formattingStackHead.gradient) return formattingStackHead.plainColor; |
||||
|
||||
indexPosition = float(index - formattingStackHead.gradientStart) / |
||||
formattingStackHead.gradientLength; |
||||
points = formattingStackHead.gradientPoints; |
||||
for (i = 1; i < points.length; i += 1) |
||||
{ |
||||
if (points[i - 1] <= indexPosition && indexPosition <= points[i]) |
||||
{ |
||||
leftPosition = points[i - 1]; |
||||
rightPosition = points[i]; |
||||
leftColor = formattingStackHead.gradientColors[i - 1]; |
||||
rightColor = formattingStackHead.gradientColors[i]; |
||||
break; |
||||
} |
||||
} |
||||
indexPosition = |
||||
(indexPosition - leftPosition) / (rightPosition - leftPosition); |
||||
targetColor.R = Lerp(indexPosition, leftColor.R, rightColor.R); |
||||
targetColor.G = Lerp(indexPosition, leftColor.G, rightColor.G); |
||||
targetColor.B = Lerp(indexPosition, leftColor.B, rightColor.B); |
||||
targetColor.A = Lerp(indexPosition, leftColor.A, rightColor.A); |
||||
return targetColor; |
||||
} |
||||
|
||||
private final function FormattingInfo ParseFormattingInfo(Text colorTag) |
||||
{ |
||||
local int i; |
||||
local Parser colorParser; |
||||
local Color nextColor; |
||||
local array<MutableText> specifiedColors; |
||||
local array<Color> gradientColors; |
||||
local array<float> gradientPoints; |
||||
local FormattingInfo targetInfo; |
||||
if (colorTag.IsEmpty()) |
||||
{ |
||||
Report(FSE_EmptyColorTag); |
||||
return targetInfo; // not colored |
||||
} |
||||
specifiedColors = colorTag.SplitByCharacter(separatorCharacter, true); |
||||
for (i = 0; i < specifiedColors.length; i += 1) |
||||
{ |
||||
colorParser = _.text.Parse(specifiedColors[i]); |
||||
if (_.color.ParseWith(colorParser, nextColor)) |
||||
{ |
||||
colorParser.Confirm(); |
||||
gradientColors[gradientColors.length] = nextColor; |
||||
gradientPoints[gradientPoints.length] = ParsePoint(colorParser); |
||||
} |
||||
else { |
||||
Report(FSE_BadColor, specifiedColors[i]); |
||||
} |
||||
_.memory.Free(colorParser); |
||||
} |
||||
_.memory.FreeMany(specifiedColors); |
||||
gradientPoints = NormalizePoints(gradientPoints); |
||||
targetInfo.colored = (gradientColors.length > 0); |
||||
targetInfo.gradient = (gradientColors.length > 1); |
||||
targetInfo.gradientColors = gradientColors; |
||||
targetInfo.gradientPoints = gradientPoints; |
||||
if (gradientColors.length > 0) { |
||||
targetInfo.plainColor = gradientColors[0]; |
||||
} |
||||
return targetInfo; |
||||
} |
||||
|
||||
private final function float ParsePoint(Parser parser) |
||||
{ |
||||
local float point; |
||||
local Parser.ParserState initialState; |
||||
if (!parser.Ok() || parser.HasFinished()) { |
||||
return -1; |
||||
} |
||||
initialState = parser.GetCurrentState(); |
||||
// [Necessary part] Should starts with "[" |
||||
if (!parser.Match(T(TOPENING_BRACKET)).Ok()) |
||||
{ |
||||
Report( |
||||
FSE_BadGradientPoint, |
||||
parser.RestoreState(initialState).GetRemainder()); |
||||
return -1; |
||||
} |
||||
// [Necessary part] Try parsing number |
||||
parser.MNumber(point).Confirm(); |
||||
if (!parser.Ok()) |
||||
{ |
||||
Report( |
||||
FSE_BadGradientPoint, |
||||
parser.RestoreState(initialState).GetRemainder()); |
||||
return -1; |
||||
} |
||||
// [Optional part] Check if number is a percentage |
||||
if (parser.Match(T(TPERCENT)).Ok()) { |
||||
point *= 0.01; |
||||
} |
||||
// This either confirms state of parsing "%" (on success) |
||||
// or reverts to the previous state, just after parsing the number |
||||
// (on failure) |
||||
parser.Confirm(); |
||||
parser.R(); |
||||
// [Necessary part] Have to have closing parenthesis |
||||
if (!parser.HasFinished()) { |
||||
parser.Match(T(TCLOSING_BRACKET)).Confirm(); |
||||
} |
||||
// Still return `point`, even if there was no closing parenthesis, |
||||
// since that is likely what user wants |
||||
if (!parser.Ok()) |
||||
{ |
||||
Report( |
||||
FSE_BadGradientPoint, |
||||
parser.RestoreState(initialState).GetRemainder()); |
||||
} |
||||
return point; |
||||
} |
||||
|
||||
private final function array<float> NormalizePoints(array<float> points) |
||||
{ |
||||
local int i, j; |
||||
local int negativeSegmentStart, negativeSegmentLength; |
||||
local float leftPositiveBound, rightPositiveBound; |
||||
local bool foundNegative; |
||||
// Leftmost and rightmost points are always fixed |
||||
if (points.length > 1) |
||||
{ |
||||
points[0] = 0.0; |
||||
points[points.length - 1] = 1.0; |
||||
} |
||||
for (i = 1; i < points.length - 1; i += 1) |
||||
{ |
||||
// Each point must be in bounds (between `0` and `1`) and points |
||||
// must be specified in an increasing order. |
||||
// If either does not hold - simply mark point as unspecified and |
||||
// let let it be regenerated naturally. |
||||
if (points[i] <= 0 || points[i] > 1 || points[i] <= points[i - 1]) { |
||||
points[i] = -1; |
||||
} |
||||
} |
||||
// Check all points - if a sequence of them are undefined, then place |
||||
// them uniformly between bounding non-negative points. |
||||
// For example [0.5, -1, -1, -1, -1, 1] should turn into |
||||
// [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]. |
||||
// NOTE: at the beginning of this method we have forced `points[0]` |
||||
// to be `0.0` and `points[points.length - 1]` to be `1.0`. Thanks to that |
||||
// there always exists left and right non-negative bounding points. |
||||
for (i = 1; i < points.length; i += 1) |
||||
{ |
||||
// Found first element of negative sequence |
||||
if (!foundNegative && points[i] < 0) |
||||
{ |
||||
leftPositiveBound = points[i - 1]; |
||||
negativeSegmentStart = i; |
||||
} |
||||
// Found where negative sequence ends |
||||
if (foundNegative && points[i] > 0) |
||||
{ |
||||
rightPositiveBound = points[i]; |
||||
for (j = negativeSegmentStart; j < i; j += 1) |
||||
{ |
||||
points[j] = Lerp( |
||||
float(j - negativeSegmentStart + 1) / |
||||
float(negativeSegmentLength + 1), |
||||
leftPositiveBound, |
||||
rightPositiveBound); |
||||
} |
||||
negativeSegmentLength = 0; |
||||
} |
||||
foundNegative = (points[i] < 0); |
||||
// Still continuing with negative segment |
||||
if (foundNegative) { |
||||
negativeSegmentLength += 1; |
||||
} |
||||
} |
||||
return points; |
||||
} |
||||
|
||||
// Following four functions are to maintain a "color stack" that will |
||||
// remember unclosed colors (new colors are obtained from formatting commands |
||||
// sequence) defined in formatted string, in order. |
||||
// Stack array always contains one element, defined by |
||||
// the `SetupFormattingStack()` call. It corresponds to the default formatting |
||||
// that will be used when we pop all the other elements. |
||||
// It is necessary to deal with possible folded formatting definitions in |
||||
// formatted strings. |
||||
private final function SetupFormattingStack(Text.Formatting defaultFormatting) |
||||
{ |
||||
local FormattingInfo defaultFormattingInfo; |
||||
defaultFormattingInfo.colored = defaultFormatting.isColored; |
||||
defaultFormattingInfo.plainColor = defaultFormatting.color; |
||||
if (formattingStack.length > 0) { |
||||
formattingStack.length = 0; |
||||
} |
||||
formattingStack[0] = defaultFormattingInfo; |
||||
formattingStackHead = defaultFormattingInfo; |
||||
} |
||||
|
||||
private final function PushIntoFormattingStack( |
||||
FormattingCommandsSequence.FormattingCommand formattingCommand) |
||||
{ |
||||
formattingStackHead = ParseFormattingInfo(formattingCommand.tag); |
||||
formattingStackHead.gradientStart = formattingCommand.openIndex; |
||||
formattingStackHead.gradientLength = |
||||
float(formattingCommand.closeIndex - formattingCommand.openIndex); |
||||
formattingStack[formattingStack.length] = formattingStackHead; |
||||
} |
||||
|
||||
private final function SwapFormattingStack(Text.Character tagCharacter) |
||||
{ |
||||
local FormattingInfo updatedFormatting; |
||||
if (formattingStack.length > 0) { |
||||
updatedFormatting = formattingStackHead; |
||||
} |
||||
if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.plainColor)) |
||||
{ |
||||
updatedFormatting.colored = true; |
||||
updatedFormatting.gradient = false; |
||||
} |
||||
else |
||||
{ |
||||
Report( |
||||
FSE_BadShortColorTag, |
||||
_.text.FromString("^" $ Chr(tagCharacter.codePoint))); |
||||
} |
||||
formattingStackHead = updatedFormatting; |
||||
if (formattingStack.length > 0) { |
||||
formattingStack[formattingStack.length - 1] = updatedFormatting; |
||||
} |
||||
else { |
||||
formattingStack[0] = updatedFormatting; |
||||
} |
||||
} |
||||
|
||||
private final function PopFormattingStack() |
||||
{ |
||||
// Remove the top of the stack |
||||
if (formattingStack.length > 0) { |
||||
formattingStack.length = formattingStack.length - 1; |
||||
} |
||||
// Update the stack head copy |
||||
if (formattingStack.length > 0) { |
||||
formattingStackHead = formattingStack[formattingStack.length - 1]; |
||||
} |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
TOPENING_BRACKET = 0 |
||||
stringConstants(0) = "[" |
||||
TCLOSING_BRACKET = 1 |
||||
stringConstants(1) = "]" |
||||
TPERCENT = 2 |
||||
stringConstants(2) = "%" |
||||
} |
Loading…
Reference in new issue