|
|
|
/**
|
|
|
|
* Acedia's implementation of an immutable text (string) object.
|
|
|
|
* Since it is not native class, it has additional costs for it's creation and
|
|
|
|
* some of it operations, but it:
|
|
|
|
* 1. Supports a more convenient (than native 4-byte color sequences)
|
|
|
|
* storing of format information and allows to extract `string`s with
|
|
|
|
* or without formatting. Including Acedia's own, more human-readable
|
|
|
|
* way to define string formatting.
|
|
|
|
* 2. Stores `string`s disassembled into Unicode code points, potentially
|
|
|
|
* allowing fast implementation of operations that require such
|
|
|
|
* a representation (e.g. faster hash calculation was implemented).
|
|
|
|
* 3. Provides an additional layer of abstraction that can potentially
|
|
|
|
* allow for an improved Unicode support.
|
|
|
|
* 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 Text extends AcediaObject;
|
|
|
|
|
|
|
|
// Enumeration that describes how should we treat `Text`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 `Text`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 `Text` 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;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Even though `Text` is an immutable object, for child classes we
|
|
|
|
* encapsulate direct access to `codePoints` by providing a way to append
|
|
|
|
* new data to it's content through protected methods.
|
|
|
|
* For that 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 `Text` to
|
|
|
|
* a specified one (`newFormatting`). This method is faster than calling
|
|
|
|
* `ReformatRange`.
|
|
|
|
*
|
|
|
|
* @param newFormatting Formatting to set to the whole `Text`.
|
|
|
|
*/
|
|
|
|
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`, te 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static method for creating an immutable `Text` object from (plain) `string`.
|
|
|
|
*
|
|
|
|
* It is preferred to use `TextAPI` methods for creating `Text` instances.
|
|
|
|
*
|
|
|
|
* @param source Plain `string` to convert into `Text`.
|
|
|
|
* @return `Text` instance (guaranteed to be not `none`) that stores contents
|
|
|
|
* of `source` if treated as a plain `string`.
|
|
|
|
*/
|
|
|
|
public static final function Text ConstFromPlainString(string source)
|
|
|
|
{
|
|
|
|
local MutableText builder;
|
|
|
|
local Text result;
|
|
|
|
builder = MutableText(__().memory.Allocate(class'MutableText'));
|
|
|
|
result = builder.AppendPlainString(source).Copy();
|
|
|
|
builder.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static method for creating an immutable `Text` object from
|
|
|
|
* (colored) `string`.
|
|
|
|
*
|
|
|
|
* It is preferred to use `TextAPI` methods for creating `Text` instances.
|
|
|
|
*
|
|
|
|
* @param source Colored `string` to convert into `Text`.
|
|
|
|
* @return `Text` instance (guaranteed to be not `none`) that stores contents
|
|
|
|
* of `source` if treated as a colored `string`.
|
|
|
|
*/
|
|
|
|
public static final function Text ConstFromColoredString(string source)
|
|
|
|
{
|
|
|
|
local MutableText builder;
|
|
|
|
local Text result;
|
|
|
|
builder = MutableText(__().memory.Allocate(class'MutableText'));
|
|
|
|
result = builder.AppendColoredString(source).Copy();
|
|
|
|
builder.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static method for creating an immutable `Text` object from
|
|
|
|
* (formatted) `string`.
|
|
|
|
*
|
|
|
|
* It is preferred to use `TextAPI` methods for creating `Text` instances.
|
|
|
|
*
|
|
|
|
* @param source Formatted `string` to convert into `Text`.
|
|
|
|
* @return `Text` instance (guaranteed to be not `none`) that stores contents
|
|
|
|
* of `source` if treated as a formatted `string`.
|
|
|
|
*/
|
|
|
|
public static final function Text ConstFromFormattedString(string source)
|
|
|
|
{
|
|
|
|
local MutableText builder;
|
|
|
|
local Text result;
|
|
|
|
builder = MutableText(__().memory.Allocate(class'MutableText'));
|
|
|
|
result = builder.AppendFormattedString(source).Copy();
|
|
|
|
builder.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Makes an immutable copy (`class'Text'`) of the caller `Text`.
|
|
|
|
*
|
|
|
|
* 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 are replaces by `MaxInt`,
|
|
|
|
* effectively extracting as much of a string as possible.
|
|
|
|
* @return Immutable copy of the caller `Text` 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;
|
|
|
|
}
|
|
|
|
endIndex = startIndex + maxLength;
|
|
|
|
copy = Text(_.memory.Allocate(class'Text'));
|
|
|
|
// Edge cases
|
|
|
|
if (endIndex <= 0) {
|
|
|
|
return copy;
|
|
|
|
}
|
|
|
|
if (startIndex <= 0 && maxLength >= startIndex + 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 text instance.
|
|
|
|
*
|
|
|
|
* 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 are replaces by `MaxInt`,
|
|
|
|
* effectively extracting as much of a string as possible.
|
|
|
|
* @return Mutable copy of the caller `Text` 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;
|
|
|
|
}
|
|
|
|
endIndex = startIndex + maxLength;
|
|
|
|
copy = MutableText(_.memory.Allocate(class'MutableText'));
|
|
|
|
// Edge cases
|
|
|
|
if (endIndex <= 0 || startIndex >= codePoints.length) {
|
|
|
|
return copy;
|
|
|
|
}
|
|
|
|
if (startIndex <= 0 && maxLength >= startIndex + 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 `Text`
|
|
|
|
* 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 are replaces by `MaxInt`,
|
|
|
|
* effectively extracting as much of a string as possible.
|
|
|
|
* @return Immutable copy of caller `Text` 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 `Text`
|
|
|
|
* in a 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 are replaces by `MaxInt`,
|
|
|
|
* effectively extracting as much of a string as possible.
|
|
|
|
* @return Immutable copy of caller `Text` 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 text 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 are replaces by `MaxInt`,
|
|
|
|
* effectively extracting as much of a string as possible.
|
|
|
|
* @return Mutable copy of caller `Text` 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 are replaces by `MaxInt`,
|
|
|
|
* effectively extracting as much of a string as possible.
|
|
|
|
* @return Mutable copy of caller `Text` 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Auxiliary function that converts case of the caller `Text` object.
|
|
|
|
* As `Text` is supposed to be immutable, cannot be public.
|
|
|
|
*
|
|
|
|
* @param toLower `true` if caller `Text` 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 `Text`. 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()
|
|
|
|
{
|
|
|
|
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 `Text` are empty.
|
|
|
|
*
|
|
|
|
* @return `true` if caller `Text` contains no symbols.
|
|
|
|
*/
|
|
|
|
public final function bool IsEmpty()
|
|
|
|
{
|
|
|
|
return (codePoints.length == 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns current length of the caller `Text` 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 `Text`'s contents in symbols.
|
|
|
|
*/
|
|
|
|
public final function int GetLength()
|
|
|
|
{
|
|
|
|
return codePoints.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Override equality check for `Text` to make two different `Text`s equal
|
|
|
|
* based on their text contents.
|
|
|
|
* Text equality check is case-sensitive and does not take formatting
|
|
|
|
* into account.
|
|
|
|
*
|
|
|
|
* `Text` cannot be equal to object of any class that is not
|
|
|
|
* derived from `Text`.
|
|
|
|
*
|
|
|
|
* @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 `Text`,
|
|
|
|
* `false` otherwise.
|
|
|
|
*/
|
|
|
|
public function bool IsEqual(Object other)
|
|
|
|
{
|
|
|
|
if (self == other) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return Compare(Text(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 `Text` 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 `Text` 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 `Text` is equal to the `otherText` under
|
|
|
|
* specified parameters and `false` otherwise.
|
|
|
|
*/
|
|
|
|
public final function bool Compare(
|
|
|
|
Text 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 `Text` 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 `Text` 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 `Text` starts with `otherText` under
|
|
|
|
* specified parameters and `false` otherwise.
|
|
|
|
*/
|
|
|
|
public final function bool StartsWith(
|
|
|
|
Text 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 ends with another `Text` 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 `Text` 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 `Text` ends with `otherText` under
|
|
|
|
* specified parameters and `false` otherwise.
|
|
|
|
*/
|
|
|
|
public final function bool EndsWith(
|
|
|
|
Text 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper method for comparing formatting data of the caller `Text`
|
|
|
|
// and `otherText`.
|
|
|
|
private final function bool CompareFormatting(Text otherText)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local array<FormattingChunk> rightChunks;
|
|
|
|
local TextAPI api;
|
|
|
|
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 `Text` 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 `Text` 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 `Text` is equal to the `stringToCompare` under
|
|
|
|
* specified parameters and `false` otherwise.
|
|
|
|
*/
|
|
|
|
public final function bool CompareToPlainString(
|
|
|
|
string stringToCompare,
|
|
|
|
optional CaseSensitivity caseSensitivity,
|
|
|
|
optional FormatSensitivity formatSensitivity)
|
|
|
|
{
|
|
|
|
local MutableText builder;
|
|
|
|
local bool result;
|
|
|
|
builder = MutableText(_.memory.Allocate(class'MutableText'));
|
|
|
|
builder.AppendPlainString(stringToCompare);
|
|
|
|
result = Compare(builder, caseSensitivity, formatSensitivity);
|
|
|
|
builder.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Method for checking equality between the caller `Text` 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 `Text` 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 `Text` 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 `Text` 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 `Text` 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 `Text` 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 `Text`'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 `Text`, to allow for method chaining.
|
|
|
|
*/
|
|
|
|
protected final function Text 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 `Text`'s data.
|
|
|
|
* Convenience method over existing `AppendCodePoint()`.
|
|
|
|
*
|
|
|
|
* Formatting of these code points needs to be set beforehand by
|
|
|
|
* `SetFormatting()` method.
|
|
|
|
*
|
|
|
|
* @return Returns caller `Text`, to allow for method chaining.
|
|
|
|
*/
|
|
|
|
protected final function Text 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 `Text`.
|
|
|
|
* Allows to easier create mutable child classes.
|
|
|
|
*
|
|
|
|
* @return Returns caller `Text`, to allow for method chaining.
|
|
|
|
*/
|
|
|
|
protected final function Text DropCodePoints() {
|
|
|
|
codePoints.length = 0;
|
|
|
|
formattingChunks.length = 0;
|
|
|
|
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 `Text`, to allow for method chaining.
|
|
|
|
*/
|
|
|
|
protected final function Text 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 `Text`.
|
|
|
|
*
|
|
|
|
* 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 `Text` 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 `Text`,
|
|
|
|
* i.e. `string` without any color information inside.
|
|
|
|
*/
|
|
|
|
public final function string ToPlainString(
|
|
|
|
optional int startIndex,
|
|
|
|
optional int maxLength)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local string result;
|
|
|
|
if (maxLength <= 0) {
|
|
|
|
maxLength = MaxInt;
|
|
|
|
}
|
|
|
|
else if (startIndex < 0)
|
|
|
|
{
|
|
|
|
maxLength += startIndex;
|
|
|
|
startIndex = 0;
|
|
|
|
}
|
|
|
|
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 `Text` 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 `Text`,
|
|
|
|
* 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 = 0;
|
|
|
|
}
|
|
|
|
// `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 `Text` 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 `Text`,
|
|
|
|
* 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 = 0;
|
|
|
|
}
|
|
|
|
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`.
|
|
|
|
* @return Array of `MutableText`s that contain separated substrings.
|
|
|
|
*/
|
|
|
|
public final function array<MutableText> SplitByCharacter(Character separator)
|
|
|
|
{
|
|
|
|
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))
|
|
|
|
{
|
|
|
|
result[result.length] = nextText;
|
|
|
|
nextText = _.text.Empty();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
nextText.AppendCharacter(nextCharacter);
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
result[result.length] = nextText;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the index position of the first occurrence of the `otherText` in
|
|
|
|
* the caller `Text`, searching forward from index position `fromIndex`.
|
|
|
|
*
|
|
|
|
* @param otherText `Text` data to find inside the caller `Text`.
|
|
|
|
* @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(
|
|
|
|
Text 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 `Text`, searching backward from index position `fromIndex`,
|
|
|
|
* counted the end of the caller `Text`.
|
|
|
|
*
|
|
|
|
* @param otherText `Text` data to find inside the caller `Text`.
|
|
|
|
* @param fromIndex Index from which we should start searching, but
|
|
|
|
* it's counted from the end of the caller `Text`: `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 `Text`).
|
|
|
|
*/
|
|
|
|
public final function int LastIndexOf(
|
|
|
|
Text 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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 // '&'
|
|
|
|
}
|