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