You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
565 lines
20 KiB
565 lines
20 KiB
/** |
|
* 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; |
|
|
|
var private int CODEPOINT_NEWLINE, CODEPOINT_ACCENT; |
|
|
|
enum FormattedStackCommandType |
|
{ |
|
// Push more data onto formatted stack |
|
FST_StackPush, |
|
// Pop data from formatted stack |
|
FST_StackPop, |
|
// Swap the top value on the formatting stack |
|
FST_StackSwap |
|
}; |
|
|
|
// Formatted `string` is separated into several (possibly nested) parts, |
|
// each with its own formatting. These can be easily handled with a formatting |
|
// stack: |
|
// * Each time a new section opens ("{<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. |
|
// Logic that parses formatted `string` works is broken into two steps: |
|
// 1. Read formatted `string` detecting "{<color_tag ", "}" and "^" |
|
// sequences and build a series of stack commands (along with data that |
|
// should be appended after them); |
|
// 2. use these commands to construct `MutableText`. |
|
struct FormattedStackCommand |
|
{ |
|
// Did this block start by opening or closing formatted part? |
|
// Ignored for the very first block without any formatting. |
|
var FormattedStackCommandType type; |
|
// Full text inside the block, without any formatting |
|
var array<int> contents; |
|
// Formatting tag for the next block |
|
// (only used for `FST_StackPush` command type) |
|
var MutableText tag; |
|
// Formatting character for the "^"-type tag |
|
// (only used for `FST_StackSwap` command type) |
|
var Character charTag; |
|
}; |
|
// Appending formatted `string` into the `MutableText` first requires its |
|
// transformation into series of `FormattedStackCommand` and then their |
|
// execution to assemble the `MutableText`. |
|
// First element of `stackCommands` is special and is used solely as |
|
// a container for unformatted data. It should not be used to execute |
|
// formatting stack commands. |
|
// This variable contains intermediary data. |
|
var array<FormattedStackCommand> stackCommands; |
|
// Formatted `string` can have an arbitrary level of folded format definitions, |
|
// this array is used as a stack to keep track of opened formatting blocks |
|
// when appending formatted `string`. |
|
var array<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 new line character to the caller `MutableText`. |
|
* |
|
* @return Caller `MutableText` to allow for method chaining. |
|
*/ |
|
public final function MutableText AppendLineBreak() |
|
{ |
|
AppendCodePoint(CODEPOINT_NEWLINE); |
|
return self; |
|
} |
|
|
|
/** |
|
* 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 AppendString( |
|
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 `Text` to the caller `MutableText`. |
|
* |
|
* @param source `Text` (with formatted string contents) to be |
|
* appended to the caller `MutableText`. |
|
* @param defaultFormatting Formatting to apply to `source`'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 AppendFormatted( |
|
Text source, |
|
optional Formatting defaultFormatting) |
|
{ |
|
local Parser parser; |
|
parser = _.text.Parse(source); |
|
AppendFormattedParser(parser, defaultFormatting); |
|
parser.FreeSelf(); |
|
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 Parser parser; |
|
parser = _.text.ParseString(source); |
|
AppendFormattedParser(parser, defaultFormatting); |
|
parser.FreeSelf(); |
|
return self; |
|
} |
|
|
|
/** |
|
* Appends contents of the formatted `string` to the caller `MutableText`. |
|
* |
|
* @param source Formatted `string` to be appended to |
|
* the caller `MutableText`. |
|
* @param defaultFormatting Formatting to be used for `source`'s characters |
|
* that have no color information defined. |
|
* @return Caller `MutableText` to allow for method chaining. |
|
*/ |
|
private final function MutableText AppendFormattedParser( |
|
Parser sourceParser, |
|
optional Formatting defaultFormatting) |
|
{ |
|
local int i; |
|
local Parser tagParser; |
|
BuildFormattingStackCommands(sourceParser); |
|
if (stackCommands.length <= 0) { |
|
return self; |
|
} |
|
SetupFormattingStack(defaultFormatting); |
|
tagParser = Parser(_.memory.Allocate(class'Parser')); |
|
SetFormatting(defaultFormatting); |
|
// First element of color stack is special and has no color information; |
|
// see `BuildFormattingStackCommands()` for details. |
|
AppendManyCodePoints(stackCommands[0].contents); |
|
for (i = 1; i < stackCommands.length; i += 1) |
|
{ |
|
if (stackCommands[i].type == FST_StackPush) |
|
{ |
|
tagParser.Initialize(stackCommands[i].tag); |
|
SetFormatting(PushIntoFormattingStack(tagParser)); |
|
} |
|
else if (stackCommands[i].type == FST_StackPop) { |
|
SetFormatting(PopFormattingStack()); |
|
} |
|
else if (stackCommands[i].type == FST_StackSwap) { |
|
SetFormatting(SwapFormattingStack(stackCommands[i].charTag)); |
|
} |
|
AppendManyCodePoints(stackCommands[i].contents); |
|
_.memory.Free(stackCommands[i].tag); |
|
} |
|
stackCommands.length = 0; |
|
_.memory.Free(tagParser); |
|
return self; |
|
} |
|
|
|
// Function that parses formatted `string` into array of |
|
// `FormattedStackCommand`s. |
|
// Returned array is guaranteed to always have at least one element. |
|
// First element in array always corresponds to part of the input string |
|
// (`source`) without any formatting defined, even if it's empty. |
|
// This is to avoid having fourth command type, only usable at the beginning. |
|
private final function BuildFormattingStackCommands(Parser parser) |
|
{ |
|
local Character nextCharacter; |
|
local FormattedStackCommand nextCommand; |
|
stackCommands.length = 0; |
|
while (!parser.HasFinished()) |
|
{ |
|
parser.MCharacter(nextCharacter); |
|
// New command by "{<color>" |
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT)) |
|
{ |
|
stackCommands[stackCommands.length] = nextCommand; |
|
nextCommand = CreateStackCommand(FST_StackPush); |
|
parser.MUntil(nextCommand.tag,, true) |
|
.MCharacter(nextCommand.charTag); // Simply to skip a char |
|
continue; |
|
} |
|
// New command by "}" |
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT)) |
|
{ |
|
stackCommands[stackCommands.length] = nextCommand; |
|
nextCommand = CreateStackCommand(FST_StackPop); |
|
continue; |
|
} |
|
// New command by "^" |
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT)) |
|
{ |
|
stackCommands[stackCommands.length] = nextCommand; |
|
nextCommand = CreateStackCommand(FST_StackSwap); |
|
parser.MCharacter(nextCommand.charTag); |
|
if (!parser.Ok()) { |
|
break; |
|
} |
|
continue; |
|
} |
|
// Escaped sequence |
|
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_FORMAT_ESCAPE)) { |
|
parser.MCharacter(nextCharacter); |
|
} |
|
if (!parser.Ok()) { |
|
break; |
|
} |
|
nextCommand.contents[nextCommand.contents.length] = |
|
nextCharacter.codePoint; |
|
} |
|
// Only put in empty command if there is nothing else. |
|
if (nextCommand.contents.length > 0 || stackCommands.length == 0) { |
|
stackCommands[stackCommands.length] = nextCommand; |
|
} |
|
} |
|
|
|
// Following four functions are to maintain a "color stack" that will |
|
// remember unclosed colors (new colors are obtained from formatting commands |
|
// sequence) defined in formatted string, in order. |
|
// Stack array always contains one element, defined by |
|
// the `SetupFormattingStack()` call. It corresponds to the default formatting |
|
// that will be used when we pop all the other elements. |
|
// It is necessary to deal with possible folded formatting definitions in |
|
// formatted strings. |
|
private final function SetupFormattingStack(Text.Formatting defaultFormatting) |
|
{ |
|
formattingStack.length = 0; |
|
formattingStack[0] = defaultFormatting; |
|
} |
|
|
|
private final function Formatting PushIntoFormattingStack( |
|
Parser formattingDefinitionParser) |
|
{ |
|
local Formatting newFormatting; |
|
if (_.color.ParseWith(formattingDefinitionParser, newFormatting.color)) { |
|
newFormatting.isColored = true; |
|
} |
|
formattingStack[formattingStack.length] = newFormatting; |
|
return newFormatting; |
|
} |
|
|
|
private final function Formatting SwapFormattingStack(Character tagCharacter) |
|
{ |
|
local Formatting updatedFormatting; |
|
if (formattingStack.length <= 0) { |
|
return updatedFormatting; |
|
} |
|
updatedFormatting = formattingStack[formattingStack.length - 1]; |
|
if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.color)) { |
|
updatedFormatting.isColored = true; |
|
} |
|
formattingStack[formattingStack.length - 1] = updatedFormatting; |
|
return updatedFormatting; |
|
} |
|
|
|
private final function Formatting PopFormattingStack() |
|
{ |
|
local Formatting result; |
|
formattingStack.length = Max(1, formattingStack.length - 1); |
|
if (formattingStack.length > 0) { |
|
result = formattingStack[formattingStack.length - 1]; |
|
} |
|
return result; |
|
} |
|
|
|
// Helper method for a quick creation of a new `FormattedStackCommand` |
|
private final function FormattedStackCommand CreateStackCommand( |
|
FormattedStackCommandType stackCommandType) |
|
{ |
|
local FormattedStackCommand newCommand; |
|
newCommand.type = stackCommandType; |
|
return newCommand; |
|
} |
|
|
|
/** |
|
* Unlike `Text`, `MutableText` can change it's content and therefore it's |
|
* hash code cannot depend on it. So we restore `AcediaObject`'s behavior and |
|
* return random value, generated at the time of allocation. |
|
* |
|
* @return Hash code for the caller `MutableText`. |
|
*/ |
|
protected function int CalculateHashCode() |
|
{ |
|
return super(AcediaObject).GetHashCode(); |
|
} |
|
|
|
/** |
|
* Replaces every occurrence of the string `before` with the string `after`. |
|
* |
|
* @param before `Text` contents to match and then replace. |
|
* @param after `Text` contents to replace `before` with. |
|
* @param caseSensitivity Defines whether `before` should be matched |
|
* in a case-sensitive manner. By default it will be. |
|
* @param formatSensitivity Defines whether `before` should be matched |
|
* in a way sensitive for color information. By default it is will not. |
|
* @return Returns caller `Text`, to allow for method chaining. |
|
*/ |
|
public final function MutableText Replace( |
|
Text before, |
|
Text after, |
|
optional CaseSensitivity caseSensitivity, |
|
optional FormatSensitivity formatSensitivity) |
|
{ |
|
local int index; |
|
local bool needToInsertReplacer; |
|
local int nextReplacementIndex; |
|
local Text selfCopy; |
|
if (before == none) return self; |
|
if (before.IsEmpty()) return self; |
|
|
|
selfCopy = Copy(); |
|
Clear(); |
|
while (index < selfCopy.GetLength()) |
|
{ |
|
nextReplacementIndex = selfCopy.IndexOf(before, index, |
|
caseSensitivity, |
|
formatSensitivity); |
|
if (nextReplacementIndex < 0) |
|
{ |
|
needToInsertReplacer = false; |
|
nextReplacementIndex = selfCopy.GetLength(); |
|
} |
|
else { |
|
needToInsertReplacer = true; |
|
} |
|
// Copy characters between replacements one by one |
|
while (index < nextReplacementIndex) |
|
{ |
|
AppendCharacter(selfCopy.GetCharacter(index)); |
|
index += 1; |
|
} |
|
if (needToInsertReplacer) |
|
{ |
|
Append(after); |
|
// `before.GetLength() > 0` because of entry conditions |
|
index += before.GetLength(); |
|
} |
|
} |
|
selfCopy.FreeSelf(); |
|
return self; |
|
} |
|
|
|
/** |
|
* Changes formatting for characters with indices in range, specified as |
|
* `[startIndex; startIndex + maxLength - 1]` to `newFormatting` parameter. |
|
* |
|
* If provided parameters `startPosition` and `maxLength` define a range that |
|
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid |
|
* range will be used. |
|
* |
|
* @param newFormatting New formatting to apply. |
|
* @param startIndex Position of the first character to change formatting |
|
* of. By default `0`, corresponding to the very first character. |
|
* @param maxLength Max length of the segment to change formatting of. |
|
* By default `0`, - that and all negative values are replaces by `MaxInt`, |
|
* effectively extracting as much of a string as possible. |
|
* @return Reference to the caller `MutableText` to allow for method chaining. |
|
*/ |
|
public final function MutableText ChangeFormatting( |
|
Formatting newFormatting, |
|
optional int startIndex, |
|
optional int maxLength) |
|
{ |
|
local int endIndex; |
|
if (startIndex >= GetLength()) { |
|
return self; |
|
} |
|
if (maxLength <= 0) { |
|
maxLength = MaxInt; |
|
} |
|
endIndex = Min(startIndex + maxLength, GetLength()) - 1; |
|
startIndex = Max(startIndex, 0); |
|
if (startIndex > endIndex) { |
|
return self; |
|
} |
|
if (startIndex == 0 && endIndex == GetLength() - 1) { |
|
ReformatWhole(newFormatting); |
|
} |
|
else { |
|
ReformatRange(startIndex, endIndex, newFormatting); |
|
} |
|
return self; |
|
} |
|
|
|
defaultproperties |
|
{ |
|
CODEPOINT_NEWLINE = 10 |
|
CODEPOINT_ACCENT = 94 |
|
} |