|
|
|
/**
|
|
|
|
* Mutable version of Acedia's `Text`
|
|
|
|
* Copyright 2020 - 2021 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 MutableText extends Text;
|
|
|
|
|
|
|
|
// Every formatted `string` essentially consists of multiple differently
|
|
|
|
// formatted (colored) parts. Such `string`s will be more convenient for us to
|
|
|
|
// work with if we separate them from each other.
|
|
|
|
// This structure represents one such block: maximum uninterrupted
|
|
|
|
// substring, every character of which has identical formatting.
|
|
|
|
// Do note that a single block does not define text formatting, -
|
|
|
|
// it is defined by the whole sequence of blocks before it
|
|
|
|
// (if `isOpening == false` you only know that you should change previous
|
|
|
|
// formatting, but you do not know to what).
|
|
|
|
struct FormattedBlock
|
|
|
|
{
|
|
|
|
// Did this block start by opening or closing formatted part?
|
|
|
|
// Ignored for the very first block without any formatting.
|
|
|
|
var bool isOpening;
|
|
|
|
// Full text inside the block, without any formatting
|
|
|
|
var array<int> contents;
|
|
|
|
// Formatting tag for this block
|
|
|
|
// (ignored for `isOpening == false`)
|
|
|
|
var string tag;
|
|
|
|
// Whitespace symbol that separates tag from the `contents`;
|
|
|
|
// For the purposes of reassembling a `string` broken into blocks.
|
|
|
|
// (ignored for `isOpening == false`)
|
|
|
|
var Character delimiter;
|
|
|
|
};
|
|
|
|
// Appending formatted `string` into the `MutableText` first requires to
|
|
|
|
// split it into series of `FormattedBlock` and then extract code points with
|
|
|
|
// the proper formatting from it.
|
|
|
|
// This variable contains intermediary data.
|
|
|
|
var array<FormattedBlock> splitBlocks;
|
|
|
|
// Formatted `string` can have an arbitrary level of folded format definitions,
|
|
|
|
// this array is used as a stack to keep track of opened formatting blocks
|
|
|
|
// when appending formatted `string`.
|
|
|
|
var array<Formatting> formattingStack;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears all current data from the caller `MutableText` instance.
|
|
|
|
*
|
|
|
|
* @return Returns caller `MutableText` to allow for method chaining.
|
|
|
|
*/
|
|
|
|
public final function MutableText Clear()
|
|
|
|
{
|
|
|
|
DropCodePoints();
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Appends a new character to the caller `MutableText`.
|
|
|
|
*
|
|
|
|
* @param newCharacter Character to add to the caller `MutableText`.
|
|
|
|
* Only valid characters will be added.
|
|
|
|
* @return Caller `MutableText` to allow for method chaining.
|
|
|
|
*/
|
|
|
|
public final function MutableText AppendCharacter(Text.Character newCharacter)
|
|
|
|
{
|
|
|
|
if (!_.text.IsValidCharacter(newCharacter)) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
SetFormatting(newCharacter.formatting);
|
|
|
|
return MutableText(AppendCodePoint(newCharacter.codePoint));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts caller `MutableText` instance into lower case.
|
|
|
|
*/
|
|
|
|
public final function ToLower()
|
|
|
|
{
|
|
|
|
ConvertCase(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts caller `MutableText` instance into upper case.
|
|
|
|
*/
|
|
|
|
public final function ToUpper()
|
|
|
|
{
|
|
|
|
ConvertCase(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Appends contents of another `Text` to the caller `MutableText`.
|
|
|
|
*
|
|
|
|
* @param other Instance of `Text`, which content method must
|
|
|
|
* append. Appends nothing if passed value is `none`.
|
|
|
|
* @param defaultFormatting Formatting to apply to `other`'s character that
|
|
|
|
* do not have it specified. For example, `defaultFormatting.isColored`,
|
|
|
|
* but some of `other`'s characters do not have a color defined -
|
|
|
|
* they will be appended with a specified color.
|
|
|
|
* @return Caller `MutableText` to allow for method chaining.
|
|
|
|
*/
|
|
|
|
public final function MutableText Append(
|
|
|
|
Text other,
|
|
|
|
optional Formatting defaultFormatting)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local int otherLength;
|
|
|
|
local Character nextCharacter;
|
|
|
|
local Formatting newFormatting;
|
|
|
|
if (other == none) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
SetFormatting(defaultFormatting);
|
|
|
|
otherLength = other.GetLength();
|
|
|
|
for (i = 0; i < otherLength; i += 1)
|
|
|
|
{
|
|
|
|
nextCharacter = other.GetRawCharacter(i);
|
|
|
|
if (other.IsFormattingChangedAt(i))
|
|
|
|
{
|
|
|
|
newFormatting = other.GetFormatting(i);
|
|
|
|
// If default formatting is specified, but `other`'s formatting
|
|
|
|
// (at least for some characters) is not, - apply default one
|
|
|
|
if (defaultFormatting.isColored && !newFormatting.isColored)
|
|
|
|
{
|
|
|
|
newFormatting.isColored = true;
|
|
|
|
newFormatting.color = defaultFormatting.color;
|
|
|
|
}
|
|
|
|
SetFormatting(newFormatting);
|
|
|
|
}
|
|
|
|
AppendCodePoint(nextCharacter.codePoint);
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Appends contents of the plain `string` to the caller `MutableText`.
|
|
|
|
*
|
|
|
|
* @param source Plain `string` to be appended to
|
|
|
|
* the caller `MutableText`.
|
|
|
|
* @param defaultFormatting Formatting to be used for `source`'s characters.
|
|
|
|
* By default defines 'null' formatting (no color set).
|
|
|
|
* @return Caller `MutableText` to allow for method chaining.
|
|
|
|
*/
|
|
|
|
public final function MutableText AppendPlainString(
|
|
|
|
string source,
|
|
|
|
optional Formatting defaultFormatting)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local int sourceLength;
|
|
|
|
|
|
|
|
sourceLength = Len(source);
|
|
|
|
SetFormatting(defaultFormatting);
|
|
|
|
// Decompose `source` into integer codes
|
|
|
|
for (i = 0; i < sourceLength; i += 1) {
|
|
|
|
AppendCodePoint(Asc(Mid(source, i, 1)));
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Appends contents of the colored `string` to the caller `MutableText`.
|
|
|
|
*
|
|
|
|
* @param source Colored `string` to be appended to
|
|
|
|
* the caller `MutableText`.
|
|
|
|
* @param defaultFormatting Formatting to be used for `source`'s characters
|
|
|
|
* that have no color information defined.
|
|
|
|
* By default defines 'null' formatting (no color set).
|
|
|
|
* @return Caller `MutableText` to allow for method chaining.
|
|
|
|
*/
|
|
|
|
public final function MutableText AppendColoredString(
|
|
|
|
string source,
|
|
|
|
optional Formatting defaultFormatting)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local int sourceLength;
|
|
|
|
local array<int> sourceAsIntegers;
|
|
|
|
local Formatting newFormatting;
|
|
|
|
|
|
|
|
// Decompose `source` into integer codes
|
|
|
|
sourceLength = Len(source);
|
|
|
|
for (i = 0; i < sourceLength; i += 1) {
|
|
|
|
sourceAsIntegers[sourceAsIntegers.length] = Asc(Mid(source, i, 1));
|
|
|
|
}
|
|
|
|
// With colored strings we only need to care about color for formatting
|
|
|
|
i = 0;
|
|
|
|
newFormatting = defaultFormatting;
|
|
|
|
SetFormatting(newFormatting);
|
|
|
|
while (i < sourceLength)
|
|
|
|
{
|
|
|
|
if (sourceAsIntegers[i] == CODEPOINT_ESCAPE)
|
|
|
|
{
|
|
|
|
if (i + 3 >= sourceLength) break;
|
|
|
|
newFormatting.isColored = true;
|
|
|
|
newFormatting.color = _.color.RGB(sourceAsIntegers[i + 1],
|
|
|
|
sourceAsIntegers[i + 2],
|
|
|
|
sourceAsIntegers[i + 3]);
|
|
|
|
i += 4;
|
|
|
|
SetFormatting(newFormatting);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
AppendCodePoint(sourceAsIntegers[i]);
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Appends contents of the formatted `string` to the caller `MutableText`.
|
|
|
|
*
|
|
|
|
* @param source Formatted `string` to be appended to
|
|
|
|
* the caller `MutableText`.
|
|
|
|
* @param defaultFormatting Formatting to be used for `source`'s characters
|
|
|
|
* that have no color information defined.
|
|
|
|
* @return Caller `MutableText` to allow for method chaining.
|
|
|
|
*/
|
|
|
|
public final function MutableText AppendFormattedString(
|
|
|
|
string source,
|
|
|
|
optional Formatting defaultFormatting)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local Parser parser;
|
|
|
|
SplitFormattedStringIntoBlocks(source);
|
|
|
|
if (splitBlocks.length <= 0) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
SetupFormattingStack(defaultFormatting);
|
|
|
|
parser = Parser(_.memory.Allocate(class'Parser'));
|
|
|
|
// First element of `decomposedSource` is special and has
|
|
|
|
// no color information,
|
|
|
|
// see `SplitFormattedStringIntoBlocks()` for details.
|
|
|
|
SetFormatting(defaultFormatting);
|
|
|
|
AppendManyCodePoints(splitBlocks[0].contents);
|
|
|
|
for (i = 1; i < splitBlocks.length; i += 1)
|
|
|
|
{
|
|
|
|
if (splitBlocks[i].isOpening)
|
|
|
|
{
|
|
|
|
parser.InitializeS(splitBlocks[i].tag);
|
|
|
|
SetFormatting(PushIntoFormattingStack(parser));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
SetFormatting(PopFormattingStack());
|
|
|
|
}
|
|
|
|
AppendManyCodePoints(splitBlocks[i].contents);
|
|
|
|
}
|
|
|
|
_.memory.Free(parser);
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Function that breaks formatted string into array of `FormattedBlock`s.
|
|
|
|
// Returned array is guaranteed to always have at least one block.
|
|
|
|
// First block in array always corresponds to part of the input string
|
|
|
|
// (`source`) without any formatting defined, even if it's empty.
|
|
|
|
// This is to avoid `FormattedBlock` having a third option besides two defined
|
|
|
|
// by `isOpening` variable.
|
|
|
|
private final function SplitFormattedStringIntoBlocks(string source)
|
|
|
|
{
|
|
|
|
local Parser parser;
|
|
|
|
local Character nextCharacter;
|
|
|
|
local FormattedBlock nextBlock;
|
|
|
|
splitBlocks.length = 0;
|
|
|
|
parser = _.text.ParseString(source);
|
|
|
|
while (!parser.HasFinished()) {
|
|
|
|
parser.MCharacter(nextCharacter);
|
|
|
|
// New formatted block by "{<color>"
|
|
|
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT))
|
|
|
|
{
|
|
|
|
splitBlocks[splitBlocks.length] = nextBlock;
|
|
|
|
nextBlock = CreateFormattedBlock(true);
|
|
|
|
parser.MUntilS(nextBlock.tag,, true)
|
|
|
|
.MCharacter(nextBlock.delimiter);
|
|
|
|
if (!parser.Ok()) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// New formatted block by "}"
|
|
|
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT))
|
|
|
|
{
|
|
|
|
splitBlocks[splitBlocks.length] = nextBlock;
|
|
|
|
nextBlock = CreateFormattedBlock(false);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Escaped sequence
|
|
|
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) {
|
|
|
|
parser.MCharacter(nextCharacter);
|
|
|
|
}
|
|
|
|
if (!parser.Ok()) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
nextBlock.contents[nextBlock.contents.length] = nextCharacter.codePoint;
|
|
|
|
}
|
|
|
|
// Only put in empty block if there is nothing else.
|
|
|
|
if (nextBlock.contents.length > 0 || splitBlocks.length == 0) {
|
|
|
|
splitBlocks[splitBlocks.length] = nextBlock;
|
|
|
|
}
|
|
|
|
_.memory.Free(parser);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Following two functions are to maintain a "color stack" that will
|
|
|
|
// remember unclosed colors (new colors are obtained from a parser) defined in
|
|
|
|
// formatted string, on 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.
|
|
|
|
// For storing the color information we simply use `Text.Character`,
|
|
|
|
// ignoring all information that is not related to colors.
|
|
|
|
private final function SetupFormattingStack(Text.Formatting defaultFormatting)
|
|
|
|
{
|
|
|
|
formattingStack.length = 0;
|
|
|
|
formattingStack[0] = defaultFormatting;
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function Formatting PushIntoFormattingStack(
|
|
|
|
Parser formattingDefinitionParser)
|
|
|
|
{
|
|
|
|
local Formatting newFormatting;
|
|
|
|
if (_.color.ParseWith(formattingDefinitionParser, newFormatting.color)) {
|
|
|
|
newFormatting.isColored = true;
|
|
|
|
}
|
|
|
|
formattingStack[formattingStack.length] = newFormatting;
|
|
|
|
return newFormatting;
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function Formatting PopFormattingStack()
|
|
|
|
{
|
|
|
|
local Formatting result;
|
|
|
|
formattingStack.length = Max(1, formattingStack.length - 1);
|
|
|
|
if (formattingStack.length > 0) {
|
|
|
|
result = formattingStack[formattingStack.length - 1];
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper method for a quick creation of a new `FormattedBlock`
|
|
|
|
private final function FormattedBlock CreateFormattedBlock(bool isOpening)
|
|
|
|
{
|
|
|
|
local FormattedBlock newBlock;
|
|
|
|
newBlock.isOpening = isOpening;
|
|
|
|
return newBlock;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unlike `Text`, `MutableText` can change it's content and therefore it's
|
|
|
|
* hash code cannot depend on it. So we restore `AcediaObject`'s behavior and
|
|
|
|
* return random value, generated at the time of allocation.
|
|
|
|
*
|
|
|
|
* @return Hash code for the caller `MutableText`.
|
|
|
|
*/
|
|
|
|
protected function int CalculateHashCode()
|
|
|
|
{
|
|
|
|
return super(AcediaObject).GetHashCode();
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultproperties
|
|
|
|
{
|
|
|
|
}
|