@ -19,37 +19,55 @@
*/
*/
class MutableText extends Text;
class MutableText extends Text;
var private int CODEPOINT_NEWLINE;
var private int CODEPOINT_NEWLINE, CODEPOINT_ACCENT ;
// Every formatted `string` essentially consists of multiple differently
enum FormattedStackCommandType
// formatted (colored) parts. Such `string`s will be more convenient for us to
{
// work with if we separate them from each other.
// Push more data onto formatted stack
// This structure represents one such block: maximum uninterrupted
FST_StackPush,
// substring, every character of which has identical formatting.
// Pop data from formatted stack
// Do note that a single block does not define text formatting, -
FST_StackPop,
// it is defined by the whole sequence of blocks before it
// Swap the top value on the formatting stack
// (if `isOpening == false` you only know that you should change previous
FST_StackSwap
// formatting, but you do not know to what).
};
struct FormattedBlock
// Formatted `string` is separated into several (possibly nested) parts,
// each with its own formatting. These can be easily handled with a formatting
// stack:
// * Each time a new section opens ("{<color_tag ") we put another,
// current formatting on top of the stack;
// * Each time a section closes ("}") we pop the stack, returning to
// a previous formatting.
// * In a special case of "^" color swap that is supposed to last until
// current block closes we simply swap the color of the formatting on
// top of the stack.
// Logic that parses formatted `string` works is broken into two steps:
// 1. Read formatted `string` detecting "{<color_tag ", "}" and "^"
// sequences and build a series of stack commands (along with data that
// should be appended after them);
// 2. use these commands to construct `MutableText`.
struct FormattedStackCommand
{
{
// Did this block start by opening or closing formatted part?
// Did this block start by opening or closing formatted part?
// Ignored for the very first block without any formatting.
// Ignored for the very first block without any formatting.
var bool isOpening;
var FormattedStackCommandType type ;
// Full text inside the block, without any formatting
// Full text inside the block, without any formatting
var array<int> contents;
var array<int> contents;
// Formatting tag for this block
// Formatting tag for the next block
// (ignored for `isOpening == false`)
// (only used for `FST_StackPush` command type)
var string tag;
var MutableText tag;
// Whitespace symbol that separates tag from the `contents`;
// Formatting character for the "^"-type tag
// For the purposes of reassembling a `string` broken into blocks.
// (only used for `FST_StackSwap` command type)
// (ignored for `isOpening == false`)
var Character charTag;
var Character delimiter;
};
};
// Appending formatted `string` into the `MutableText` first requires to
// Appending formatted `string` into the `MutableText` first requires its
// split it into series of `FormattedBlock` and then extract code points with
// transformation into series of `FormattedStackCommand` and then their
// the proper formatting from it.
// execution to assemble the `MutableText`.
// First element of `stackCommands` is special and is used solely as
// a container for unformatted data. It should not be used to execute
// formatting stack commands.
// This variable contains intermediary data.
// This variable contains intermediary data.
var array<FormattedBlock> splitBlocks;
var array<FormattedStackCommand> stackCommand s;
// Formatted `string` can have an arbitrary level of folded format definitions,
// Formatted `string` can have an arbitrary level of folded format definitions,
// this array is used as a stack to keep track of opened formatting blocks
// this array is used as a stack to keep track of opened formatting blocks
// when appending formatted `string`.
// when appending formatted `string`.
@ -227,6 +245,28 @@ public final function MutableText AppendColoredString(
return self;
return self;
}
}
/**
* Appends contents of the formatted `Text` to the caller `MutableText`.
*
* @param source `Text` (with formatted string contents) to be
* appended to the caller `MutableText`.
* @param defaultFormatting Formatting to apply to `source`'s character that
* do not have it specified. For example, `defaultFormatting.isColored`,
* but some of `other`'s characters do not have a color defined -
* they will be appended with a specified color.
* @return Caller `MutableText` to allow for method chaining.
*/
public final function MutableText AppendFormatted(
Text source,
optional Formatting defaultFormatting)
{
local Parser parser;
parser = _.text.Parse(source);
AppendFormattedParser(parser, defaultFormatting);
parser.FreeSelf();
return self;
}
/**
/**
* Appends contents of the formatted `string` to the caller `MutableText`.
* Appends contents of the formatted `string` to the caller `MutableText`.
*
*
@ -240,68 +280,98 @@ public final function MutableText AppendFormattedString(
string source,
string source,
optional Formatting defaultFormatting)
optional Formatting defaultFormatting)
{
{
local int i;
local Parser parser;
local Parser parser;
SplitFormattedStringIntoBlocks(source);
parser = _.text.ParseString(source);
if (splitBlocks.length <= 0) {
AppendFormattedParser(parser, defaultFormatting);
parser.FreeSelf();
return self;
}
/**
* Appends contents of the formatted `string` to the caller `MutableText`.
*
* @param source Formatted `string` to be appended to
* the caller `MutableText`.
* @param defaultFormatting Formatting to be used for `source`'s characters
* that have no color information defined.
* @return Caller `MutableText` to allow for method chaining.
*/
private final function MutableText AppendFormattedParser(
Parser sourceParser,
optional Formatting defaultFormatting)
{
local int i;
local Parser tagParser;
BuildFormattingStackCommands(sourceParser);
if (stackCommands.length <= 0) {
return self;
return self;
}
}
SetupFormattingStack(defaultFormatting);
SetupFormattingStack(defaultFormatting);
parser = Parser(_.memory.Allocate(class'Parser'));
tagParser = Parser(_.memory.Allocate(class'Parser'));
// First element of `decomposedSource` is special and has
// no color information,
// see `SplitFormattedStringIntoBlocks()` for details.
SetFormatting(defaultFormatting);
SetFormatting(defaultFormatting);
AppendManyCodePoints(splitBlocks[0].contents);
// First element of color stack is special and has no color information;
for (i = 1; i < splitBlocks.length; i += 1)
// see `BuildFormattingStackCommands()` for details.
AppendManyCodePoints(stackCommands[0].contents);
for (i = 1; i < stackCommands.length; i += 1)
{
{
if (splitBlocks[i].isOpening)
if (stackCommands[i].type == FST_StackPush )
{
{
parser.InitializeS(splitBlocks[i].tag);
tagParser.Initialize(stackCommand s[i].tag);
SetFormatting(PushIntoFormattingStack(parser));
SetFormatting(PushIntoFormattingStack(tagP arser));
}
}
else {
else if (stackCommands[i].type == FST_StackPop) {
SetFormatting(PopFormattingStack());
SetFormatting(PopFormattingStack());
}
}
AppendManyCodePoints(splitBlocks[i].contents);
else if (stackCommands[i].type == FST_StackSwap) {
SetFormatting(SwapFormattingStack(stackCommands[i].charTag));
}
AppendManyCodePoints(stackCommands[i].contents);
_.memory.Free(stackCommands[i].tag);
}
}
_.memory.Free(parser);
stackCommands.length = 0;
_.memory.Free(tagParser);
return self;
return self;
}
}
// Function that breaks formatted string into array of `FormattedBlock`s.
// Function that parses formatted `string` into array of
// Returned array is guaranteed to always have at least one block.
// `FormattedStackCommand`s.
// First block in array always corresponds to part of the input string
// Returned array is guaranteed to always have at least one element.
// First element in array always corresponds to part of the input string
// (`source`) without any formatting defined, even if it's empty.
// (`source`) without any formatting defined, even if it's empty.
// This is to avoid `FormattedBlock` having a third option besides two defined
// This is to avoid having fourth command type, only usable at the beginning.
// by `isOpening` variable.
private final function BuildFormattingStackCommands(Parser parser)
private final function SplitFormattedStringIntoBlocks(string source)
{
{
local Parser parser;
local Character nextCharacter;
local Character nextCharacter;
local FormattedBlock nextBlock;
local FormattedStackCommand nextCommand;
splitBlocks.length = 0;
stackCommands.length = 0;
parser = _.text.ParseString(source);
while (!parser.HasFinished())
while (!parser.HasFinished())
{
{
parser.MCharacter(nextCharacter);
parser.MCharacter(nextCharacter);
// New formatted block by "{<color>"
// New command by "{<color>"
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT))
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_OPEN_FORMAT))
{
{
splitBlocks[splitBlocks.length] = nextBlock;
stackCommands[stackCommands.length] = nextCommand;
nextBlock = CreateFormattedBlock(true);
nextCommand = CreateStackCommand(FST_StackPush);
parser.MUntilS(nextBlock.tag,, true)
parser.MUntil(nextCommand.tag,, true)
.MCharacter(nextBlock.delimiter);
.MCharacter(nextCommand.charTag); // Simply to skip a char
if (!parser.Ok()) {
break;
}
continue;
continue;
}
}
// New formatted block by "}"
// New command by "}"
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT))
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_CLOSE_FORMAT))
{
{
splitBlocks[splitBlocks.length] = nextBlock;
stackCommands[stackCommands.length] = nextCommand;
nextBlock = CreateFormattedBlock(false);
nextCommand = CreateStackCommand(FST_StackPop);
continue;
}
// New command by "^"
if (_.text.IsCodePoint(nextCharacter, CODEPOINT_ACCENT))
{
stackCommands[stackCommands.length] = nextCommand;
nextCommand = CreateStackCommand(FST_StackSwap);
parser.MCharacter(nextCommand.charTag);
if (!parser.Ok()) {
break;
}
continue;
continue;
}
}
// Escaped sequence
// Escaped sequence
@ -311,25 +381,23 @@ private final function SplitFormattedStringIntoBlocks(string source)
if (!parser.Ok()) {
if (!parser.Ok()) {
break;
break;
}
}
nextBlock.contents[nextBlock.contents.length] = nextCharacter.codePoint;
nextCommand.contents[nextCommand.contents.length] =
nextCharacter.codePoint;
}
}
// Only put in empty block if there is nothing else.
// Only put in empty command if there is nothing else.
if (nextBlock.contents.length > 0 || splitBlock s.length == 0) {
if (nextCommand.contents.length > 0 || stackCommand s.length == 0) {
splitBlocks[splitBlocks.length] = nextBlock ;
stackCommands[stackCommands.length] = nextCommand ;
}
}
_.memory.Free(parser);
}
}
// Following two functions are to maintain a "color stack" that will
// Following four functions are to maintain a "color stack" that will
// remember unclosed colors (new colors are obtained from a parser) defined in
// remember unclosed colors (new colors are obtained from formatting commands
// formatted string, o n order.
// sequence) defined in formatted string, i n order.
// Stack array always contains one element, defined by
// Stack array always contains one element, defined by
// the `SetupFormattingStack()` call. It corresponds to the default formatting
// the `SetupFormattingStack()` call. It corresponds to the default formatting
// that will be used when we pop all the other elements.
// that will be used when we pop all the other elements.
// It is necessary to deal with possible folded formatting definitions in
// It is necessary to deal with possible folded formatting definitions in
// formatted strings.
// formatted strings.
// For storing the color information we simply use `Text.Character`,
// ignoring all information that is not related to colors.
private final function SetupFormattingStack(Text.Formatting defaultFormatting)
private final function SetupFormattingStack(Text.Formatting defaultFormatting)
{
{
formattingStack.length = 0;
formattingStack.length = 0;
@ -347,6 +415,20 @@ private final function Formatting PushIntoFormattingStack(
return newFormatting;
return newFormatting;
}
}
private final function Formatting SwapFormattingStack(Character tagCharacter)
{
local Formatting updatedFormatting;
if (formattingStack.length <= 0) {
return updatedFormatting;
}
updatedFormatting = formattingStack[formattingStack.length - 1];
if (_.color.ResolveShortTagColor(tagCharacter, updatedFormatting.color)) {
updatedFormatting.isColored = true;
}
formattingStack[formattingStack.length - 1] = updatedFormatting;
return updatedFormatting;
}
private final function Formatting PopFormattingStack()
private final function Formatting PopFormattingStack()
{
{
local Formatting result;
local Formatting result;
@ -357,12 +439,13 @@ private final function Formatting PopFormattingStack()
return result;
return result;
}
}
// Helper method for a quick creation of a new `FormattedBlock`
// Helper method for a quick creation of a new `FormattedStackCommand`
private final function FormattedBlock CreateFormattedBlock(bool isOpening)
private final function FormattedStackCommand CreateStackCommand(
FormattedStackCommandType stackCommandType)
{
{
local FormattedBlock newBlock ;
local FormattedStackCommand newCommand ;
newBlock.isOpening = isOpening ;
newCommand.type = stackCommandType ;
return newBlock ;
return newCommand ;
}
}
/**
/**
@ -478,4 +561,5 @@ public final function MutableText ChangeFormatting(
defaultproperties
defaultproperties
{
{
CODEPOINT_NEWLINE = 10
CODEPOINT_NEWLINE = 10
CODEPOINT_ACCENT = 94
}
}