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.
1519 lines
53 KiB
1519 lines
53 KiB
2 years ago
|
/**
|
||
|
* Acedia's base text class. It implements all of the methods for
|
||
|
* immutable `Text`, but by itself does not give a guarantee of immutability,
|
||
|
* since `MutableText` is one of its child classes.
|
||
|
* Copyright 2020 - 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 BaseText extends AcediaObject
|
||
|
abstract;
|
||
|
|
||
|
// Enumeration that describes how should we treat `BaseText`s that
|
||
|
// differ in case only.
|
||
|
// By default we would consider them unequal.
|
||
|
enum CaseSensitivity
|
||
|
{
|
||
|
SCASE_SENSITIVE,
|
||
|
SCASE_INSENSITIVE
|
||
|
};
|
||
|
|
||
|
// Enumeration that describes how should we treat `BaseText`s that
|
||
|
// differ only in their formatting.
|
||
|
// By default we would consider them unequal.
|
||
|
enum FormatSensitivity
|
||
|
{
|
||
|
SFORM_INSENSITIVE,
|
||
|
SFORM_SENSITIVE
|
||
|
};
|
||
|
|
||
|
// Describes text formatting, can be applied per-character.
|
||
|
struct Formatting
|
||
|
{
|
||
|
// Whether this formatting describes a color of the text
|
||
|
var bool isColored;
|
||
|
// Color of the text, used only when `isColored == true`
|
||
|
var Color color;
|
||
|
};
|
||
|
|
||
|
// Represents one character, together with it's formatting
|
||
|
struct Character
|
||
|
{
|
||
|
var int codePoint;
|
||
|
var Formatting formatting;
|
||
|
};
|
||
|
|
||
|
// Actual content of the `BaseText` is stored as a sequence of
|
||
|
// Unicode code points.
|
||
|
var private array<int> codePoints;
|
||
|
// Structure for inner use, describes a change of formatting to `formatting`,
|
||
|
// starting from index `startIndex`
|
||
|
struct FormattingChunk
|
||
|
{
|
||
|
var private int startIndex;
|
||
|
var private Formatting formatting;
|
||
|
};
|
||
|
// Series of `FormattingChunk` that defines formatting over characters,
|
||
|
// defined by `codePoints`.
|
||
|
// This array is sorted by `startIndex` of it's elements and
|
||
|
// formatting for character at <index> is defined by the `FormattingChunk`
|
||
|
// with the largest `startIndex <= <index>`.
|
||
|
var private array<FormattingChunk> formattingChunks;
|
||
|
// To optimize look up of formatting for characters we remember index of
|
||
|
// the last used `FormattingChunk` to attempt to start the next lookup
|
||
|
// from that point.
|
||
|
// This improves performance when several lookups are done in order.
|
||
|
var private int formattingIndexCache;
|
||
|
|
||
|
/*
|
||
|
* In the base class we implement `AppendCodePoint()` and
|
||
|
* `AppendManyCodePoints()` methods that allow to add code points 1-by-1.
|
||
|
* To reduce the amount of checks, formatting is set not per-code point,
|
||
|
* but separately by `SetFormatting()`: to append a group of code points with
|
||
|
* a certain formatting one has to first set their formatting,
|
||
|
* then just add code points.
|
||
|
*/
|
||
|
// Formatting to use for the next code point
|
||
|
var private Formatting nextFormatting;
|
||
|
// `true` if the next code point will have a different formatting from
|
||
|
// the last added one.
|
||
|
var private bool formattingUpdated;
|
||
|
|
||
|
// Escape code point is used to change output's color and is used in
|
||
|
// Unreal Engine's `string`s.
|
||
|
var protected const int CODEPOINT_ESCAPE;
|
||
|
// Opening and closing symbols for colored blocks in formatted strings.
|
||
|
var protected const int CODEPOINT_OPEN_FORMAT;
|
||
|
var protected const int CODEPOINT_CLOSE_FORMAT;
|
||
|
var protected const string STRING_OPEN_FORMAT;
|
||
|
var protected const string STRING_CLOSE_FORMAT;
|
||
|
// Symbol for separating opening formatting block from it's contents
|
||
|
var protected const string STRING_SEPARATOR_FORMAT;
|
||
|
// Symbol to escape any character in formatted strings,
|
||
|
// including above mentioned opening and closing symbols.
|
||
|
var protected const int CODEPOINT_FORMAT_ESCAPE;
|
||
|
var protected const string STRING_FORMAT_ESCAPE;
|
||
|
|
||
|
// Simply free all used memory.
|
||
|
protected function Finalizer()
|
||
|
{
|
||
|
codePoints.length = 0;
|
||
|
formattingChunks.length = 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Auxiliary method that changes formatting of the whole `BaseText` to
|
||
|
* a specified one (`newFormatting`). This method is faster than calling
|
||
|
* `ReformatRange`.
|
||
|
*
|
||
|
* @param newFormatting Formatting to set to the whole `BaseText`.
|
||
|
*/
|
||
|
protected final function ReformatWhole(Formatting newFormatting)
|
||
|
{
|
||
|
local FormattingChunk newChunk;
|
||
|
formattingChunks.length = 0;
|
||
|
newChunk.startIndex = 0;
|
||
|
newChunk.formatting = newFormatting;
|
||
|
formattingChunks[0] = newChunk;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Auxiliary method that changes formatting of the characters with indices in
|
||
|
* range `[start; end]` to a specified one (`newFormatting`).
|
||
|
*
|
||
|
* This method assumes, but does not check that:
|
||
|
* 1. `start <= end`;
|
||
|
* 2. `start` and `end` parameters belong to the range of valid indices
|
||
|
* `[0; GetLength() - 1]`
|
||
|
*
|
||
|
* @param start First character to change formatting of.
|
||
|
* @param end Last character to change formatting of.
|
||
|
* @param newFormatting Formatting to set to the specified characters.
|
||
|
*/
|
||
|
protected final function ReformatRange(
|
||
|
int start,
|
||
|
int end,
|
||
|
Formatting newFormatting)
|
||
|
{
|
||
|
local int i;
|
||
|
local Formatting formattingAfterChangedSegment;
|
||
|
local FormattingChunk newChunk;
|
||
|
local array<FormattingChunk> newFormattingChunks;
|
||
|
start = Max(start, 0);
|
||
|
end = Min(GetLength() - 1, end);
|
||
|
// Formatting right after `end`, the end of re-formatted segment
|
||
|
formattingAfterChangedSegment = GetFormatting(end + 1);
|
||
|
// 1. Copy old formatting before `start`
|
||
|
for (i = 0; i < formattingChunks.length; i += 1)
|
||
|
{
|
||
|
if (start <= formattingChunks[i].startIndex) {
|
||
|
break;
|
||
|
}
|
||
|
newFormattingChunks[newFormattingChunks.length] = formattingChunks[i];
|
||
|
}
|
||
|
newChunk.formatting = newFormatting;
|
||
|
newChunk.startIndex = start;
|
||
|
newFormattingChunks[newFormattingChunks.length] = newChunk;
|
||
|
if (end == GetLength() - 1)
|
||
|
{
|
||
|
formattingChunks = newFormattingChunks;
|
||
|
// We have inserted `FormattingChunk` without checking if it actually
|
||
|
// changes formatting. It might be excessive, so do a normalization.
|
||
|
NormalizeFormatting();
|
||
|
return;
|
||
|
}
|
||
|
// 2. Drop old formatting overwritten by `newFormatting`
|
||
|
while (i < formattingChunks.length)
|
||
|
{
|
||
|
if (end < formattingChunks[i].startIndex) {
|
||
|
break;
|
||
|
}
|
||
|
i += 1;
|
||
|
}
|
||
|
// 3. Copy old formatting after `end`
|
||
|
newChunk.formatting = formattingAfterChangedSegment;
|
||
|
newChunk.startIndex = end + 1; // end < GetLength() - 1
|
||
|
newFormattingChunks[newFormattingChunks.length] = newChunk;
|
||
|
while (i < formattingChunks.length)
|
||
|
{
|
||
|
newFormattingChunks[newFormattingChunks.length] = formattingChunks[i];
|
||
|
i += 1;
|
||
|
}
|
||
|
formattingChunks = newFormattingChunks;
|
||
|
// We have inserted `FormattingChunk` without checking if it actually
|
||
|
// changes formatting. It might be excessive, so do a normalization.
|
||
|
NormalizeFormatting();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes an immutable copy (`class'Text'`) of the caller `BaseText`.
|
||
|
*
|
||
|
* Copies characters in the range `[startIndex; startIndex + maxLength - 1]`
|
||
|
* If provided parameters `startIndex` and `maxLength` define a range that
|
||
|
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
|
||
|
* range will be used.
|
||
|
*
|
||
|
* @param startIndex Position of the first character to copy.
|
||
|
* By default `0`, corresponding to the very first character.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values mean that method should extract all
|
||
|
* characters to the right of `startIndex`.
|
||
|
* @return Immutable copy of the caller `BaseText` instance.
|
||
|
* Guaranteed to be not `none` and have class `Text`.
|
||
|
*/
|
||
|
public final function Text Copy(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
// `startIndex` is inclusive and `endIndex` is not
|
||
|
local int i, endIndex;
|
||
|
local Text copy;
|
||
|
local Character nextCharacter;
|
||
|
if (maxLength <= 0) {
|
||
|
maxLength = codePoints.length - startIndex;
|
||
|
}
|
||
|
endIndex = startIndex + maxLength;
|
||
|
copy = Text(_.memory.Allocate(class'Text'));
|
||
|
// Edge cases
|
||
|
if (endIndex <= 0) {
|
||
|
return copy;
|
||
|
}
|
||
|
if (startIndex <= 0 && startIndex + maxLength >= codePoints.length)
|
||
|
{
|
||
|
copy.codePoints = codePoints;
|
||
|
copy.formattingChunks = formattingChunks;
|
||
|
return copy;
|
||
|
}
|
||
|
// Substring copy
|
||
|
if (startIndex < 0) {
|
||
|
startIndex = 0;
|
||
|
}
|
||
|
endIndex = Min(endIndex, codePoints.length);
|
||
|
for (i = startIndex; i < endIndex; i += 1)
|
||
|
{
|
||
|
nextCharacter = GetCharacter(i);
|
||
|
copy.SetFormatting(nextCharacter.formatting);
|
||
|
copy.AppendCodePoint(nextCharacter.codePoint);
|
||
|
}
|
||
|
return copy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes a mutable copy (`class'MutableText'`) of the caller
|
||
|
* `BaseText` instance.
|
||
|
*
|
||
|
* If provided parameters `startIndex` and `maxLength` define a range that
|
||
|
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
|
||
|
* range will be used.
|
||
|
*
|
||
|
* @param startIndex Position of the first character to copy.
|
||
|
* By default `0`, corresponding to the very first character.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values mean that method should extract all
|
||
|
* characters to the right of `startIndex`.
|
||
|
* @return Mutable copy of the caller `BaseText` instance.
|
||
|
* Guaranteed to be not `none` and have class `MutableText`.
|
||
|
*/
|
||
|
public final function MutableText MutableCopy(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
// `startIndex` is inclusive and `endIndex` is not
|
||
|
local int i, endIndex;
|
||
|
local MutableText copy;
|
||
|
if (maxLength <= 0) {
|
||
|
maxLength = codePoints.length - startIndex;
|
||
|
}
|
||
|
endIndex = startIndex + maxLength;
|
||
|
copy = MutableText(_.memory.Allocate(class'MutableText'));
|
||
|
// Edge cases
|
||
|
if (endIndex <= 0 || startIndex >= codePoints.length) {
|
||
|
return copy;
|
||
|
}
|
||
|
if (startIndex <= 0 && startIndex + maxLength >= codePoints.length)
|
||
|
{
|
||
|
copy.codePoints = codePoints;
|
||
|
copy.formattingChunks = formattingChunks;
|
||
|
return copy;
|
||
|
}
|
||
|
// Substring copy
|
||
|
if (startIndex < 0) {
|
||
|
startIndex = 0;
|
||
|
}
|
||
|
endIndex = Min(endIndex, codePoints.length);
|
||
|
for (i = startIndex; i < endIndex; i += 1) {
|
||
|
copy.AppendCharacter(GetCharacter(i));
|
||
|
}
|
||
|
return copy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes an immutable copy (`class'Text'`) of the caller `BaseText`
|
||
|
* in a lower case.
|
||
|
*
|
||
|
* 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 startPosition Position of the first character to copy.
|
||
|
* By default `0`, corresponding to the very first character.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values mean that method should extract all
|
||
|
* characters to the right of `startIndex`.
|
||
|
* @return Immutable copy of caller `BaseText` in a lower case.
|
||
|
* Guaranteed to be not `none` and have class `Text`.
|
||
|
*/
|
||
|
public final function Text LowerCopy(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
local Text textCopy;
|
||
|
textCopy = Copy(startIndex, maxLength);
|
||
|
textCopy.ConvertCase(true);
|
||
|
return textCopy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes an immutable copy (`class'Text'`) of the caller `BaseText`
|
||
|
* in an upper case.
|
||
|
*
|
||
|
* 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 startPosition Position of the first character to copy.
|
||
|
* By default `0`, corresponding to the very first character.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values mean that method should extract all
|
||
|
* characters to the right of `startIndex`.
|
||
|
* @return Immutable copy of caller `BaseText` in a upper case.
|
||
|
* Guaranteed to be not `none` and have class `Text`.
|
||
|
*/
|
||
|
public final function Text UpperCopy(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
local Text textCopy;
|
||
|
textCopy = Copy(startIndex, maxLength);
|
||
|
textCopy.ConvertCase(false);
|
||
|
return textCopy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes a mutable copy (`class'MutableText'`) of the caller
|
||
|
* `BaseText` instance in lower case.
|
||
|
*
|
||
|
* 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 startPosition Position of the first character to copy.
|
||
|
* By default `0`, corresponding to the very first character.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values mean that method should extract all
|
||
|
* characters to the right of `startIndex`.
|
||
|
* @return Mutable copy of caller `BaseText` instance in lower case.
|
||
|
* Guaranteed to be not `none` and have class `MutableText`.
|
||
|
*/
|
||
|
public final function MutableText LowerMutableCopy(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
local MutableText textCopy;
|
||
|
textCopy = MutableCopy(startIndex, maxLength);
|
||
|
textCopy.ConvertCase(true);
|
||
|
return textCopy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Makes a mutable copy (`class'MutableText'`) of the caller text instance
|
||
|
* in upper case.
|
||
|
*
|
||
|
* 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 startPosition Position of the first character to copy.
|
||
|
* By default `0`, corresponding to the very first character.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values mean that method should extract all
|
||
|
* characters to the right of `startIndex`.
|
||
|
* @return Mutable copy of caller `BaseText` instance in upper case.
|
||
|
* Guaranteed to be not `none` and have class `MutableText`.
|
||
|
*/
|
||
|
public final function MutableText UpperMutableCopy(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
local MutableText textCopy;
|
||
|
textCopy = MutableCopy(startIndex, maxLength);
|
||
|
textCopy.ConvertCase(false);
|
||
|
return textCopy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks if caller `BaseText` contains a valid name for a config object or
|
||
|
* not.
|
||
|
*
|
||
|
* Valid names contain only ASCII characters, digits and '.' / '_' characters.
|
||
|
* Empty `BaseText` is not considered a valid name.
|
||
|
*
|
||
|
* @return `true` if caller `BaseText` contains a valid config object name and
|
||
|
* `false` otherwise.
|
||
|
*/
|
||
|
public final function bool IsValidConfigName()
|
||
|
{
|
||
|
local int i;
|
||
|
local int codePoint;
|
||
|
local bool isValidCodePoint;
|
||
|
local Character nextCharacter;
|
||
|
if (IsEmpty()) {
|
||
|
return false;
|
||
|
}
|
||
|
for (i = 0; i < GetLength(); i += 1)
|
||
|
{
|
||
|
nextCharacter = GetCharacter(i);
|
||
|
codePoint = nextCharacter.codePoint;
|
||
|
isValidCodePoint =
|
||
|
( (codePoint == 0x2E) || (codePoint == 0x5F) // '.' or '_'
|
||
|
|| (0x30 <= codePoint && codePoint <= 0x39) // '0' to '9'
|
||
|
|| (0x41 <= codePoint && codePoint <= 0x5A) // 'A' to 'Z'
|
||
|
|| (0x61 <= codePoint && codePoint <= 0x7A)); // 'a' to 'z'
|
||
|
if (!isValidCodePoint) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Auxiliary function that converts case of the caller `BaseText` object.
|
||
|
* As `BaseText` is supposed to be immutable, cannot be public.
|
||
|
*
|
||
|
* @param toLower `true` if caller `BaseText` must be converted to
|
||
|
* the lower case and `false` otherwise.
|
||
|
*/
|
||
|
protected final function ConvertCase(bool toLower)
|
||
|
{
|
||
|
local int i;
|
||
|
local Character nextCharacter;
|
||
|
for (i = 0; i < GetLength(); i += 1)
|
||
|
{
|
||
|
nextCharacter = GetCharacter(i);
|
||
|
if (toLower) {
|
||
|
nextCharacter = _.text.ToLower(nextCharacter);
|
||
|
}
|
||
|
else {
|
||
|
nextCharacter = _.text.ToUpper(nextCharacter);
|
||
|
}
|
||
|
codePoints[i] = nextCharacter.codePoint;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Calculates hash value of the caller `BaseText`. Hash will depend only on
|
||
|
* it's textual contents, without taking formatting (color information)
|
||
|
* into account.
|
||
|
*
|
||
|
* @return Hash, that depends on textual contents only.
|
||
|
*/
|
||
|
protected function int CalculateHashCode()
|
||
|
{
|
||
|
// We have left this method here, since only the base class has access
|
||
|
// to code points, which allows for more efficient implementation.
|
||
|
// You must overload it for mutable text child classes
|
||
|
// (like `MutableText`).
|
||
|
local int i;
|
||
|
local int hash;
|
||
|
// Manually inline `CombineHash()`, to avoid too many calls
|
||
|
// (and infinite loop detection) for some long text data.
|
||
|
hash = 5381;
|
||
|
for (i = 0; i < codePoints.length; i += 1)
|
||
|
{
|
||
|
// hash * 33 + codePoints[i]
|
||
|
hash = ((hash << 5) + hash) + codePoints[i];
|
||
|
}
|
||
|
return hash;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether contents of the caller `BaseText` are empty.
|
||
|
*
|
||
|
* @return `true` if caller `BaseText` contains no symbols.
|
||
|
*/
|
||
|
public final function bool IsEmpty()
|
||
|
{
|
||
|
return (codePoints.length == 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns current length of the caller `BaseText` in symbols.
|
||
|
*
|
||
|
* Do note that codepoint is not the same as a symbol, since one symbol
|
||
|
* can consist of several code points. While current implementation only
|
||
|
* supports symbols represented by a single code point, this might change
|
||
|
* in the future.
|
||
|
*
|
||
|
* @return Current length of caller `BaseText`'s contents in symbols.
|
||
|
*/
|
||
|
public final function int GetLength()
|
||
|
{
|
||
|
return codePoints.length;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Override equality check for `BaseText` to make two different
|
||
|
* `BaseText`s equal based on their text contents.
|
||
|
* Text equality check is case-sensitive and does not take formatting
|
||
|
* into account.
|
||
|
*
|
||
|
* `BaseText` cannot be equal to object of any class that is not
|
||
|
* derived from `BaseText`.
|
||
|
*
|
||
|
* @param other Object to compare to the caller.
|
||
|
* `none` is only equal to the `none`.
|
||
|
* @return `true` if `other` is considered equal to the caller `BaseText`,
|
||
|
* `false` otherwise.
|
||
|
*/
|
||
|
public function bool IsEqual(Object other)
|
||
|
{
|
||
|
if (self == other) {
|
||
|
return true;
|
||
|
}
|
||
|
return Compare(BaseText(other));
|
||
|
}
|
||
|
|
||
|
// "Normalizes" code point for comparison by converting it to lower case if
|
||
|
// `caseSensitivity == SCASE_INSENSITIVE`.
|
||
|
// Otherwise returns same code point.
|
||
|
private final function int NormalizeCodePoint(
|
||
|
int codePoint,
|
||
|
CaseSensitivity caseSensitivity)
|
||
|
{
|
||
|
local int newCodePoint;
|
||
|
if (caseSensitivity == SCASE_INSENSITIVE) {
|
||
|
newCodePoint = class'UnicodeData'.static.ToLowerCodePoint(codePoint);
|
||
|
}
|
||
|
else {
|
||
|
newCodePoint = codePoint;
|
||
|
}
|
||
|
if (newCodePoint < 0) {
|
||
|
return codePoint;
|
||
|
}
|
||
|
return newCodePoint;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking equality between the caller and another `BaseText`
|
||
|
* object.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case and difference in formatting (color of the characters).
|
||
|
* By default comparison is case-sensitive, but ignores
|
||
|
* formatting information.
|
||
|
*
|
||
|
* @param otherText `BaseText` to compare caller instance to.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `true` if the caller `BaseText` is equal to the `otherText` under
|
||
|
* specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool Compare(
|
||
|
BaseText otherText,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local int i;
|
||
|
local array<int> otherCodePoints;
|
||
|
if (otherText == none) {
|
||
|
return false;
|
||
|
}
|
||
|
if (GetLength() != otherText.GetLength()) {
|
||
|
return false;
|
||
|
}
|
||
|
if (formatSensitivity == SFORM_SENSITIVE && !CompareFormatting(otherText)) {
|
||
|
return false;
|
||
|
}
|
||
|
// Copy once to avoid doing it each iteration
|
||
|
otherCodePoints = otherText.codePoints;
|
||
|
for (i = 0; i < codePoints.length; i += 1)
|
||
|
{
|
||
|
if ( NormalizeCodePoint(codePoints[i], caseSensitivity)
|
||
|
!= NormalizeCodePoint(otherCodePoints[i], caseSensitivity)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking if the caller starts with another `BaseText` object.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case and difference in formatting (color of the characters).
|
||
|
* By default comparison is case-sensitive, but ignores
|
||
|
* formatting information.
|
||
|
*
|
||
|
* @param otherText `BaseText` that caller is checked to start with.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `true` if the caller `BaseText` starts with `otherText` under
|
||
|
* specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool StartsWith(
|
||
|
BaseText otherText,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local int i;
|
||
|
local Character char1, char2;
|
||
|
if (otherText == none) {
|
||
|
return false;
|
||
|
}
|
||
|
if (GetLength() < otherText.GetLength()) {
|
||
|
return false;
|
||
|
}
|
||
|
for (i = 0; i < otherText.GetLength(); i += 1)
|
||
|
{
|
||
|
char1 = GetCharacter(i);
|
||
|
char2 = otherText.GetCharacter(i);
|
||
|
if ( NormalizeCodePoint(char1.codePoint, caseSensitivity)
|
||
|
!= NormalizeCodePoint(char2.codePoint, caseSensitivity)) {
|
||
|
return false;
|
||
|
}
|
||
|
if ( formatSensitivity == SFORM_SENSITIVE
|
||
|
&& !_.text.IsFormattingEqual(char1.formatting, char2.formatting)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking if the caller starts with another `string`.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case. By default comparison is case-sensitive.
|
||
|
*
|
||
|
* @param otherString `string` that caller is checked to start with.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @return `true` if the caller `BaseText` starts with `otherString` under
|
||
|
* specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool StartsWithS(
|
||
|
string otherString,
|
||
|
optional CaseSensitivity caseSensitivity)
|
||
|
{
|
||
|
local bool result;
|
||
|
local MutableText otherText;
|
||
|
otherText = _.text.FromStringM(otherString);
|
||
|
result = StartsWith(otherText, caseSensitivity);
|
||
|
_.memory.Free(otherText);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking if the caller ends with another `BaseText` object.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case and difference in formatting (color of the characters).
|
||
|
* By default comparison is case-sensitive, but ignores
|
||
|
* formatting information.
|
||
|
*
|
||
|
* @param otherText `BaseText` that caller is checked to end with.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `true` if the caller `BaseText` ends with `otherText` under
|
||
|
* specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool EndsWith(
|
||
|
BaseText otherText,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local int index, otherIndex;
|
||
|
local Character char1, char2;
|
||
|
if (otherText == none) {
|
||
|
return false;
|
||
|
}
|
||
|
if (GetLength() < otherText.GetLength()) {
|
||
|
return false;
|
||
|
}
|
||
|
index = GetLength() - 1;
|
||
|
otherIndex = otherText.GetLength() - 1;
|
||
|
while (otherIndex >= 0)
|
||
|
{
|
||
|
char1 = GetCharacter(index);
|
||
|
char2 = otherText.GetCharacter(otherIndex);
|
||
|
if ( NormalizeCodePoint(char1.codePoint, caseSensitivity)
|
||
|
!= NormalizeCodePoint(char2.codePoint, caseSensitivity)) {
|
||
|
return false;
|
||
|
}
|
||
|
if ( formatSensitivity == SFORM_SENSITIVE
|
||
|
&& !_.text.IsFormattingEqual(char1.formatting, char2.formatting)) {
|
||
|
return false;
|
||
|
}
|
||
|
index -= 1;
|
||
|
otherIndex -= 1;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking if the caller ends with another `string`.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case. By default comparison is case-sensitive.
|
||
|
*
|
||
|
* @param otherString `string` that caller is checked to end with.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @return `true` if the caller `BaseText` ends with `otherString` under
|
||
|
* specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool EndsWithS(
|
||
|
string otherString,
|
||
|
optional CaseSensitivity caseSensitivity)
|
||
|
{
|
||
|
local bool result;
|
||
|
local MutableText otherText;
|
||
|
otherText = _.text.FromStringM(otherString);
|
||
|
result = EndsWith(otherText, caseSensitivity);
|
||
|
_.memory.Free(otherText);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
// Helper method for comparing formatting data of the caller `BaseText`
|
||
|
// and `otherText`.
|
||
|
private final function bool CompareFormatting(BaseText otherText)
|
||
|
{
|
||
|
local int i;
|
||
|
local TextAPI api;
|
||
|
local array<FormattingChunk> rightChunks;
|
||
|
rightChunks = otherText.formattingChunks;
|
||
|
if (formattingChunks.length != rightChunks.length) {
|
||
|
return false;
|
||
|
}
|
||
|
api = _.text;
|
||
|
for (i = 0; i < formattingChunks.length; i += 1)
|
||
|
{
|
||
|
if (formattingChunks[i].startIndex != rightChunks[i].startIndex) {
|
||
|
return false;
|
||
|
}
|
||
|
if (!api.IsFormattingEqual( formattingChunks[i].formatting,
|
||
|
rightChunks[i].formatting))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking equality between the caller `BaseText` and
|
||
|
* a (plain) `string`.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case and difference in formatting (color of the characters).
|
||
|
* By default comparison is case-sensitive, but ignores
|
||
|
* formatting information.
|
||
|
*
|
||
|
* @param otherText Plain `string` to compare caller `BaseText` to.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `true` if the caller `BaseText` is equal to the `stringToCompare`
|
||
|
* under specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool CompareToString(
|
||
|
string stringToCompare,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local MutableText builder;
|
||
|
local bool result;
|
||
|
builder = MutableText(_.memory.Allocate(class'MutableText'));
|
||
|
builder.AppendString(stringToCompare);
|
||
|
result = Compare(builder, caseSensitivity, formatSensitivity);
|
||
|
builder.FreeSelf();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking equality between the caller `BaseText` and
|
||
|
* a (colored) `string`.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case and difference in formatting (color of the characters).
|
||
|
* By default comparison is case-sensitive, but ignores
|
||
|
* formatting information.
|
||
|
*
|
||
|
* @param otherText Colored `string` to compare caller
|
||
|
* `BaseText` to.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `true` if the caller `BaseText` is equal to the `stringToCompare`
|
||
|
* under specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool CompareToColoredString(
|
||
|
string stringToCompare,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local MutableText builder;
|
||
|
local bool result;
|
||
|
builder = MutableText(_.memory.Allocate(class'MutableText'));
|
||
|
builder.AppendColoredString(stringToCompare);
|
||
|
result = Compare(builder, caseSensitivity, formatSensitivity);
|
||
|
builder.FreeSelf();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Method for checking equality between the caller `BaseText` and
|
||
|
* a (formatted) `string`.
|
||
|
*
|
||
|
* This method supports comparison both sensitive and not sensitive to
|
||
|
* the case and difference in formatting (color of the characters).
|
||
|
* By default comparison is case-sensitive, but ignores
|
||
|
* formatting information.
|
||
|
*
|
||
|
* @param otherText Formatted `string` to compare caller
|
||
|
* `BaseText` to.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `true` if the caller `BaseText` is equal to the `stringToCompare`
|
||
|
* under specified parameters and `false` otherwise.
|
||
|
*/
|
||
|
public final function bool CompareToFormattedString(
|
||
|
string stringToCompare,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local MutableText builder;
|
||
|
local bool result;
|
||
|
builder = MutableText(_.memory.Allocate(class'MutableText'));
|
||
|
builder.AppendFormattedString(stringToCompare);
|
||
|
result = Compare(builder, caseSensitivity, formatSensitivity);
|
||
|
builder.FreeSelf();
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns character at position given by `position`.
|
||
|
*
|
||
|
* Character is returned along with it's color information.
|
||
|
*
|
||
|
* If you do not care about color, you might want to use
|
||
|
* `GetRawCharacter()` method that's slightly faster.
|
||
|
*
|
||
|
* Does some optimizations to speed up lookup of the characters,
|
||
|
* when they are looked up in order of increasing `position`.
|
||
|
*
|
||
|
* @param position Position of the character to return.
|
||
|
* First character is at `0`.
|
||
|
* @return Character at required position. If `position` was out of bounds
|
||
|
* (`< 0` or `>= self.GetLength()`), returns invalid character instead.
|
||
|
*/
|
||
|
public final function Character GetCharacter(int position)
|
||
|
{
|
||
|
local Character result;
|
||
|
if (position < 0) return _.text.GetInvalidCharacter();
|
||
|
if (position >= codePoints.length) return _.text.GetInvalidCharacter();
|
||
|
|
||
|
result.codePoint = codePoints[position];
|
||
|
result.formatting = GetFormatting(position);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns character at position given by `position`.
|
||
|
*
|
||
|
* Character is returned without it's color information
|
||
|
* (guaranteed to have `.formatting.isColored == false`).
|
||
|
*
|
||
|
* Unlike `GetCharacter()` does not optimizations.
|
||
|
*
|
||
|
* @param position Position of the character to return.
|
||
|
* First character is at `0`.
|
||
|
* @return Character at required position, without any color information.
|
||
|
* If `position` was out of bounds (`< 0` or `>= self.GetLength()`),
|
||
|
* returns invalid character instead.
|
||
|
*/
|
||
|
public final function Character GetRawCharacter(int position)
|
||
|
{
|
||
|
local Character result;
|
||
|
if (position < 0) return _.text.GetInvalidCharacter();
|
||
|
if (position >= codePoints.length) return _.text.GetInvalidCharacter();
|
||
|
|
||
|
result.codePoint = codePoints[position];
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Appends new code point to the `BaseText`'s data.
|
||
|
* Allows to create mutable child classes.
|
||
|
*
|
||
|
* Formatting of this code point needs to be set beforehand by
|
||
|
* `SetFormatting()` method.
|
||
|
*
|
||
|
* @param codePoint Code point to append, does nothing for
|
||
|
* invalid (`< 1`) code points.
|
||
|
* @return Returns caller `BaseText`, to allow for method chaining.
|
||
|
*/
|
||
|
protected final function BaseText AppendCodePoint(int codePoint)
|
||
|
{
|
||
|
local FormattingChunk newChunk;
|
||
|
codePoints[codePoints.length] = codePoint;
|
||
|
if (formattingUpdated)
|
||
|
{
|
||
|
newChunk.startIndex = codePoints.length - 1;
|
||
|
newChunk.formatting = nextFormatting;
|
||
|
formattingChunks[formattingChunks.length] = newChunk;
|
||
|
formattingUpdated = false;
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Appends several new code points to the `BaseText`'s data.
|
||
|
* Convenience method over existing `AppendCodePoint()`.
|
||
|
*
|
||
|
* Formatting of these code points needs to be set beforehand by
|
||
|
* `SetFormatting()` method.
|
||
|
*
|
||
|
* @return Returns caller `BaseText`, to allow for method chaining.
|
||
|
*/
|
||
|
protected final function BaseText AppendManyCodePoints(array<int> codePoints)
|
||
|
{
|
||
|
local int i;
|
||
|
for (i = 0; i < codePoints.length; i += 1) {
|
||
|
AppendCodePoint(codePoints[i]);
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Drops all of the code points in the caller `BaseText`.
|
||
|
* Allows to easier create mutable child classes.
|
||
|
*
|
||
|
* @return Returns caller `BaseText`, to allow for method chaining.
|
||
|
*/
|
||
|
protected final function BaseText DropCodePoints() {
|
||
|
codePoints.length = 0;
|
||
|
formattingChunks.length = 0;
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets `newFormatting` for every non-formatted character in
|
||
|
* the caller `BaseText`.
|
||
|
* Allows to create mutable child classes.
|
||
|
*
|
||
|
* @param newFormatting Formatting to use for all non-formatted character in
|
||
|
* the caller `BaseText`. If `newFormatting` is not colored itself -
|
||
|
* method does nothing.
|
||
|
* @return Returns caller `BaseText`, to allow for method chaining.
|
||
|
*/
|
||
|
protected final function BaseText _changeDefaultFormatting(
|
||
|
Formatting newFormatting)
|
||
|
{
|
||
|
local int i;
|
||
|
local FormattingChunk newFormattingChunk;
|
||
|
local array<FormattingChunk> newFormattingChunks;
|
||
|
if (!newFormatting.isColored) {
|
||
|
return self;
|
||
|
}
|
||
|
newFormattingChunk.formatting = newFormatting;
|
||
|
if ( formattingChunks.length <= 0
|
||
|
|| formattingChunks[0].startIndex > 0)
|
||
|
{
|
||
|
newFormattingChunks[0] = newFormattingChunk;
|
||
|
}
|
||
|
while (i < formattingChunks.length)
|
||
|
{
|
||
|
if (formattingChunks[i].formatting.isColored)
|
||
|
{
|
||
|
newFormattingChunks[newFormattingChunks.length] =
|
||
|
formattingChunks[i];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
newFormattingChunk.startIndex = formattingChunks[i].startIndex;
|
||
|
newFormattingChunks[newFormattingChunks.length] =
|
||
|
newFormattingChunk;
|
||
|
}
|
||
|
i += 1;
|
||
|
}
|
||
|
formattingChunks = newFormattingChunks;
|
||
|
NormalizeFormatting();
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets formatting for the next code point(s) to be added by
|
||
|
* `AppendCodePoint()` / `AppendManyCodePoints()`.
|
||
|
* Allows to create mutable child classes.
|
||
|
*
|
||
|
* Formatting is not reset by `Append...` calls, i.e. if you are adding
|
||
|
* several code points with the same formatting in a row, you only need to
|
||
|
* call `SetFormatting()` once.
|
||
|
*
|
||
|
* @param newFormatting Formatting to use for next code points,
|
||
|
* added via `AppendCodePoint()` / `AppendManyCodePoints()` methods.
|
||
|
* @return Returns caller `BaseText`, to allow for method chaining.
|
||
|
*/
|
||
|
protected final function BaseText SetFormatting(
|
||
|
optional Formatting newFormatting)
|
||
|
{
|
||
|
local Formatting lastFormatting;
|
||
|
nextFormatting = newFormatting;
|
||
|
if (formattingChunks.length > 0)
|
||
|
{
|
||
|
lastFormatting =
|
||
|
formattingChunks[formattingChunks.length - 1].formatting;
|
||
|
}
|
||
|
formattingUpdated = !_.text.IsFormattingEqual( lastFormatting,
|
||
|
nextFormatting);
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether formatting has changed at character at position `position`
|
||
|
* (different from formatting of a character before it).
|
||
|
*
|
||
|
* Helps to compose `string`s from caller `BaseText`.
|
||
|
*
|
||
|
* If this method is called for the first character (`position == 0`),
|
||
|
* then method checks whether that character has any formatting setup
|
||
|
* (has a defined color).
|
||
|
*
|
||
|
* @param position Position of the character to check. Starts from `0`.
|
||
|
* @return `true` if formatting of specified character is different from
|
||
|
* the previous one (for the first character - if it has a defined color).
|
||
|
* `false` if formatting is the same or specified `position` is
|
||
|
* out-of-bounds (`< 0` or `>= self.GetLength()`)
|
||
|
*/
|
||
|
protected final function bool IsFormattingChangedAt(int position)
|
||
|
{
|
||
|
local int i;
|
||
|
UpdateFormattingCacheFor(position);
|
||
|
for (i = formattingIndexCache; i < formattingChunks.length; i += 1)
|
||
|
{
|
||
|
if (formattingChunks[i].startIndex > position) {
|
||
|
return false;
|
||
|
}
|
||
|
if (formattingChunks[i].startIndex == position) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns formatting information of character at position `position`.
|
||
|
*
|
||
|
* Does some optimizations to speed up lookup of the formatting,
|
||
|
* when they are looked up in order of increasing `position`.
|
||
|
*
|
||
|
* @param position Position of the character to get formatting for.
|
||
|
* Starts from `0`.
|
||
|
* @return Formatting of requested character. Default formatting with
|
||
|
* undefined color, if `position is out-of-bounds
|
||
|
* (`< 0` or `>= self.GetLength()`).
|
||
|
*/
|
||
|
public final function Formatting GetFormatting(int position)
|
||
|
{
|
||
|
local int i;
|
||
|
local Formatting result;
|
||
|
UpdateFormattingCacheFor(position);
|
||
|
for (i = formattingIndexCache; i < formattingChunks.length; i += 1)
|
||
|
{
|
||
|
if (formattingChunks[i].startIndex > position) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
i -= 1;
|
||
|
if (i < 0) {
|
||
|
return result;
|
||
|
}
|
||
|
return formattingChunks[i].formatting;
|
||
|
}
|
||
|
|
||
|
// Verifies that current `formattingIndexCache` can be used as a starting
|
||
|
// position to look up for formatting of symbol at `index`.
|
||
|
// If it cannot - resets it to zero.
|
||
|
private final function UpdateFormattingCacheFor(int index)
|
||
|
{
|
||
|
if ( formattingIndexCache < 0
|
||
|
|| formattingIndexCache >= formattingChunks.length) {
|
||
|
formattingIndexCache = 0;
|
||
|
return;
|
||
|
}
|
||
|
if (formattingChunks[formattingIndexCache].startIndex > index) {
|
||
|
formattingIndexCache = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Removes possible unnecessary chunks from `formattingChunks`:
|
||
|
// if there is a chunk that tells us to have red color after index `3` and
|
||
|
// next one tells us to have red color after index `5` - the second chunk is
|
||
|
// unnecessary.
|
||
|
private final function NormalizeFormatting()
|
||
|
{
|
||
|
local int i;
|
||
|
while (i < formattingChunks.length - 1)
|
||
|
{
|
||
|
if (_.text.IsFormattingEqual( formattingChunks[i].formatting,
|
||
|
formattingChunks[i + 1].formatting))
|
||
|
{
|
||
|
formattingChunks.Remove(i + 1, 1);
|
||
|
}
|
||
|
else {
|
||
|
i += 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts data from the caller `BaseText` instance into a plain `string`.
|
||
|
* Can be used to extract only substrings.
|
||
|
*
|
||
|
* If provided parameters `startIndex` and `maxLength` define a range that
|
||
|
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
|
||
|
* range will be used.
|
||
|
*
|
||
|
* @param startIndex Position of the first symbol to extract into a
|
||
|
* plain `string`. By default `0`, corresponding to the first symbol.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values are replaces by `MaxInt`,
|
||
|
* effectively extracting as much of a `string` as possible.
|
||
|
* @return Plain `string` representation of the caller `BaseText`,
|
||
|
* i.e. `string` without any color information inside.
|
||
|
*/
|
||
|
public final function string ToString(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
local int i;
|
||
|
local string result;
|
||
|
if (maxLength <= 0) {
|
||
|
maxLength = MaxInt;
|
||
|
}
|
||
|
else if (startIndex < 0) {
|
||
|
maxLength += startIndex;
|
||
|
}
|
||
|
startIndex = Max(0, startIndex);
|
||
|
for (i = startIndex; i < codePoints.length; i += 1)
|
||
|
{
|
||
|
if (maxLength <= 0) {
|
||
|
break;
|
||
|
}
|
||
|
maxLength -= 1;
|
||
|
result $= Chr(codePoints[i]);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts data from the caller `BaseText` instance into a colored `string`.
|
||
|
* Can be used to extract only substrings.
|
||
|
*
|
||
|
* Guaranteed to add a color tag (possibly of `defaultColor`parameter)
|
||
|
* at the beginning of the returned `string`.
|
||
|
*
|
||
|
* If provided parameters `startIndex` and `maxLength` define a range that
|
||
|
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
|
||
|
* range will be used.
|
||
|
*
|
||
|
* @param startIndex Position of the first symbol to extract into a
|
||
|
* colored `string`. By default `0`, corresponding to the first symbol.
|
||
|
* @param maxLength Max length of the extracted string. By default `0`,
|
||
|
* - that and all negative values are replaces by `MaxInt`,
|
||
|
* effectively extracting as much of a `string` as possible.
|
||
|
* NOTE: this parameter only counts actual visible symbols, ignoring
|
||
|
* 4-byte color code sequences, so method `Len()`, applied to
|
||
|
* the result of `ToColoredString()`, will return a bigger value
|
||
|
* than `maxLength`.
|
||
|
* @param defaultColor Color to be applied to the parts of the string that
|
||
|
* do not have any specified color.
|
||
|
* This is necessary, since 4-byte color sequences cannot unset the color.
|
||
|
* @return Colored `string` representation of the caller `BaseText`,
|
||
|
* i.e. `string` without any color information inside.
|
||
|
*/
|
||
|
public final function string ToColoredString(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength,
|
||
|
optional Color defaultColor)
|
||
|
{
|
||
|
local int i;
|
||
|
local Formatting newFormatting;
|
||
|
local Color nextColor, appliedColor;
|
||
|
local string result;
|
||
|
if (maxLength <= 0) {
|
||
|
maxLength = MaxInt;
|
||
|
}
|
||
|
else if (startIndex < 0) {
|
||
|
maxLength += startIndex;
|
||
|
}
|
||
|
startIndex = Max(0, startIndex);
|
||
|
// `appliedColor` will contain perfect black and so,
|
||
|
// guaranteed to be different from any actually used color
|
||
|
defaultColor = _.color.FixColor(defaultColor);
|
||
|
for (i = startIndex; i < codePoints.length; i += 1)
|
||
|
{
|
||
|
if (maxLength <= 0) {
|
||
|
break;
|
||
|
}
|
||
|
maxLength -= 1;
|
||
|
if (IsFormattingChangedAt(i) || i == startIndex)
|
||
|
{
|
||
|
newFormatting = GetFormatting(i);
|
||
|
if (newFormatting.isColored) {
|
||
|
nextColor = _.color.FixColor(newFormatting.color);
|
||
|
}
|
||
|
else {
|
||
|
nextColor = defaultColor;
|
||
|
}
|
||
|
// Colors are already fixed (and will be different from
|
||
|
// `appliedColor` before we initialize it)
|
||
|
if (!_.color.AreEqual(nextColor, appliedColor))
|
||
|
{
|
||
|
result $= Chr(CODEPOINT_ESCAPE);
|
||
|
result $= Chr(nextColor.r);
|
||
|
result $= Chr(nextColor.g);
|
||
|
result $= Chr(nextColor.b);
|
||
|
appliedColor = nextColor;
|
||
|
}
|
||
|
}
|
||
|
result $= Chr(codePoints[i]);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts data from the caller `BaseText` instance into a formatted `string`.
|
||
|
* Can be used to extract only substrings.
|
||
|
*
|
||
|
* If provided parameters `startIndex` and `maxLength` define a range that
|
||
|
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
|
||
|
* range will be used.
|
||
|
*
|
||
|
* @param startIndex Position of the first symbol to extract into a
|
||
|
* formatted `string`. By default `0`, corresponding to the first symbol.
|
||
|
* @param maxLength Max length of the extracted `string`.
|
||
|
* By default `0` - that and all negative values are replaces by `MaxInt`,
|
||
|
* effectively extracting as much of a `string` as possible.
|
||
|
* NOTE: this parameter only counts actual visible symbols,
|
||
|
* ignoring formatting blocks ('{<color> }')
|
||
|
* or escape sequences (i.e. '&{' is one character),
|
||
|
* so method `Len()`, applied to the result of
|
||
|
* `ToFormattedString()`, will return a bigger value
|
||
|
* than `maxLength`.
|
||
|
* @return Formatted `string` representation of the caller `BaseText`,
|
||
|
* i.e. `string` without any color information inside.
|
||
|
*/
|
||
|
public final function string ToFormattedString(
|
||
|
optional int startIndex,
|
||
|
optional int maxLength)
|
||
|
{
|
||
|
local int i;
|
||
|
local bool isInsideBlock;
|
||
|
local string result;
|
||
|
local Formatting newFormatting;
|
||
|
if (maxLength <= 0) {
|
||
|
maxLength = MaxInt;
|
||
|
}
|
||
|
else if (startIndex < 0) {
|
||
|
maxLength += startIndex;
|
||
|
}
|
||
|
startIndex = Max(0, startIndex);
|
||
|
for (i = startIndex; i < codePoints.length; i += 1)
|
||
|
{
|
||
|
if (maxLength <= 0) {
|
||
|
break;
|
||
|
}
|
||
|
maxLength -= 1;
|
||
|
if (IsFormattingChangedAt(i) || i == startIndex)
|
||
|
{
|
||
|
newFormatting = GetFormatting(i);
|
||
|
if (isInsideBlock && i != startIndex) {
|
||
|
result $= STRING_CLOSE_FORMAT;
|
||
|
isInsideBlock = false;
|
||
|
}
|
||
|
if (newFormatting.isColored) {
|
||
|
result $= STRING_OPEN_FORMAT
|
||
|
$ _.color.ToString(newFormatting.color)
|
||
|
$ STRING_SEPARATOR_FORMAT;
|
||
|
isInsideBlock = true;
|
||
|
}
|
||
|
}
|
||
|
if ( codePoints[i] == CODEPOINT_OPEN_FORMAT
|
||
|
|| codePoints[i] == CODEPOINT_CLOSE_FORMAT) {
|
||
|
result $= STRING_FORMAT_ESCAPE;
|
||
|
}
|
||
|
result $= Chr(codePoints[i]);
|
||
|
}
|
||
|
if (isInsideBlock) {
|
||
|
result $= STRING_CLOSE_FORMAT;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Splits the string into substrings wherever `separator` occurs, and returns
|
||
|
* array of those strings.
|
||
|
*
|
||
|
* If `separator` does not match anywhere in the string, method returns a
|
||
|
* single-element array containing copy of this `Text`.
|
||
|
*
|
||
|
* @param separator Character that separates different parts of this `Text`.
|
||
|
* @param skipEmpty Set this to `true` to filter out empty `MutableText`s
|
||
|
* from the output.
|
||
|
* @return Array of `MutableText`s that contain separated substrings.
|
||
|
*/
|
||
|
public final function array<MutableText> SplitByCharacter(
|
||
|
Character separator,
|
||
|
optional bool skipEmpty)
|
||
|
{
|
||
|
local int i, length;
|
||
|
local Character nextCharacter;
|
||
|
local MutableText nextText;
|
||
|
local array<MutableText> result;
|
||
|
length = GetLength();
|
||
|
nextText = _.text.Empty();
|
||
|
i = 0;
|
||
|
while (i < length)
|
||
|
{
|
||
|
nextCharacter = GetCharacter(i);
|
||
|
if (_.text.AreEqual(separator, nextCharacter))
|
||
|
{
|
||
|
if (!skipEmpty || !nextText.IsEmpty()) {
|
||
|
result[result.length] = nextText;
|
||
|
}
|
||
|
else {
|
||
|
_.memory.Free(nextText);
|
||
|
}
|
||
|
nextText = _.text.Empty();
|
||
|
}
|
||
|
else {
|
||
|
nextText.AppendCharacter(nextCharacter);
|
||
|
}
|
||
|
i += 1;
|
||
|
}
|
||
|
if (!skipEmpty || !nextText.IsEmpty()) {
|
||
|
result[result.length] = nextText;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the index position of the first occurrence of the `otherText` in
|
||
|
* the caller `BaseText`, searching forward from index position `fromIndex`.
|
||
|
*
|
||
|
* @param otherText `BaseText` data to find inside the caller
|
||
|
* `BaseText`.
|
||
|
* @param fromIndex Index from which we should start searching.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `-1` if `otherText` is not found after `fromIndex`.
|
||
|
*/
|
||
|
public final function int IndexOf(
|
||
|
BaseText otherText,
|
||
|
optional int fromIndex,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local int startCandidate;
|
||
|
local int index, otherIndex;
|
||
|
local Character char1, char2;
|
||
|
if (otherText == none) return -1;
|
||
|
if (fromIndex > GetLength()) return -1;
|
||
|
if (GetLength() - fromIndex < otherText.GetLength()) return -1;
|
||
|
if (otherText.IsEmpty()) return fromIndex;
|
||
|
|
||
|
startCandidate = fromIndex;
|
||
|
for (index = fromIndex; index < GetLength(); index += 1)
|
||
|
{
|
||
|
char1 = GetCharacter(index);
|
||
|
char2 = otherText.GetCharacter(otherIndex);
|
||
|
if ( NormalizeCodePoint(char1.codePoint, caseSensitivity)
|
||
|
!= NormalizeCodePoint(char2.codePoint, caseSensitivity))
|
||
|
{
|
||
|
startCandidate = index + 1;
|
||
|
otherIndex = 0;
|
||
|
continue;
|
||
|
}
|
||
|
if ( formatSensitivity == SFORM_SENSITIVE
|
||
|
&& !_.text.IsFormattingEqual(char1.formatting, char2.formatting))
|
||
|
{
|
||
|
startCandidate = index + 1;
|
||
|
otherIndex = 0;
|
||
|
continue;
|
||
|
}
|
||
|
otherIndex += 1;
|
||
|
if (otherIndex == otherText.GetLength()) {
|
||
|
return startCandidate;
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the index position of the last occurrence of the `otherText` in
|
||
|
* the caller `BaseText`, searching backward from index position `fromIndex`,
|
||
|
* counted the end of the caller `BaseText`.
|
||
|
*
|
||
|
* @param otherText `BaseText` data to find inside the caller
|
||
|
* `BaseText`.
|
||
|
* @param fromIndex Index from which we should start searching, but
|
||
|
* it's counted from the end of the caller `BaseText`: `0` means starting
|
||
|
* from the last character, `1` means next to last, etc.
|
||
|
* @param caseSensitivity Defines whether comparison should be
|
||
|
* case-sensitive. By default it is.
|
||
|
* @param formatSensitivity Defines whether comparison should be
|
||
|
* sensitive for color information. By default it is not.
|
||
|
* @return `-1` if `otherText` is not found starting `fromIndex`
|
||
|
* (this index is counted from the end of the caller `BaseText`).
|
||
|
*/
|
||
|
public final function int LastIndexOf(
|
||
|
BaseText otherText,
|
||
|
optional int fromIndex,
|
||
|
optional CaseSensitivity caseSensitivity,
|
||
|
optional FormatSensitivity formatSensitivity)
|
||
|
{
|
||
|
local int startCandidate;
|
||
|
local int index, otherIndex;
|
||
|
local Character char1, char2;
|
||
|
if (otherText == none) return -1;
|
||
|
if (fromIndex > GetLength()) return -1;
|
||
|
if (GetLength() - fromIndex < otherText.GetLength()) return -1;
|
||
|
if (otherText.IsEmpty()) return fromIndex;
|
||
|
|
||
|
otherIndex = otherText.GetLength() - 1;
|
||
|
startCandidate = GetLength() - fromIndex - 1;
|
||
|
for (index = startCandidate; index >= 0; index -= 1)
|
||
|
{
|
||
|
char1 = GetCharacter(index);
|
||
|
char2 = otherText.GetCharacter(otherIndex);
|
||
|
if ( NormalizeCodePoint(char1.codePoint, caseSensitivity)
|
||
|
!= NormalizeCodePoint(char2.codePoint, caseSensitivity))
|
||
|
{
|
||
|
startCandidate = index - 1;
|
||
|
otherIndex = otherText.GetLength() - 1;
|
||
|
continue;
|
||
|
}
|
||
|
if ( formatSensitivity == SFORM_SENSITIVE
|
||
|
&& !_.text.IsFormattingEqual(char1.formatting, char2.formatting))
|
||
|
{
|
||
|
startCandidate = index - 1;
|
||
|
otherIndex = otherText.GetLength() - 1;
|
||
|
continue;
|
||
|
}
|
||
|
otherIndex -= 1;
|
||
|
if (otherIndex < 0) {
|
||
|
return startCandidate - (otherText.GetLength() - 1);
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method frees caller `MutableText` and returns immutable `Text`
|
||
|
* copy instead.
|
||
|
*
|
||
|
* @return Immutable `Text` copy of the caller `MutableText`.
|
||
|
*/
|
||
|
public function Text IntoText()
|
||
|
{
|
||
|
return none;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method frees caller `Text` and returns immutable `Text` copy instead.
|
||
|
*
|
||
|
* @return Immutable `Text` copy of the caller `MutableText`.
|
||
|
*/
|
||
|
public function MutableText IntoMutableText()
|
||
|
{
|
||
|
return none;
|
||
|
}
|
||
|
|
||
|
defaultproperties
|
||
|
{
|
||
|
STRING_SEPARATOR_FORMAT = " "
|
||
|
STRING_OPEN_FORMAT = "{"
|
||
|
STRING_CLOSE_FORMAT = "}"
|
||
|
STRING_FORMAT_ESCAPE = "&"
|
||
|
CODEPOINT_ESCAPE = 27 // ASCII escape code
|
||
|
CODEPOINT_OPEN_FORMAT = 123 // '{'
|
||
|
CODEPOINT_CLOSE_FORMAT = 125 // '}'
|
||
|
CODEPOINT_FORMAT_ESCAPE = 38 // '&'
|
||
|
}
|