Browse Source

Add ability to read formattes data from `Text`

Previously one could only read formatted data directly from `string`s.
This patch adds another method `AppendFormatted()` for appending a
`Text`/`MutableText`, while parsing its data like a formatted string.
pull/8/head
Anton Tarasenko 3 years ago
parent
commit
7e9a5f16b8
  1. 240
      sources/Text/MutableText.uc
  2. 29
      sources/Text/Tests/TEST_Text.uc

240
sources/Text/MutableText.uc

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

29
sources/Text/Tests/TEST_Text.uc

@ -44,10 +44,16 @@ protected static function TESTS()
}
protected static function Test_TextCreation()
{
Context("Testing basic functionality for creating `Text` objects.");
SubTest_TextCreationSimply();
SubTest_TextCreationFormattedComplex();
}
protected static function SubTest_TextCreationSimply()
{
local string plainString, coloredString, formattedString;
local Text plain, colored, formatted;
Context("Testing basic functionality for creating `Text` objects.");
Issue("`Text` object is not properly created from the plain string.");
plainString = "Prepare to DIE and be reborn!";
plain = class'Text'.static.ConstFromPlainString(plainString);
@ -62,10 +68,27 @@ protected static function Test_TextCreation()
TEST_ExpectTrue(colored.ToColoredString() == coloredString);
Issue("`Text` object is not properly created from the formatted string.");
formattedString = "Prepare to {rgb(255,0,0) DIE} and be reborn!";
formattedString = "Prepare to {rgb(255,0,0) DIE ^yand} be ^7reborn!";
formatted = class'Text'.static.ConstFromFormattedString(formattedString);
TEST_ExpectNotNone(formatted);
TEST_ExpectTrue(formatted.ToFormattedString() == formattedString);
TEST_ExpectTrue( formatted.ToFormattedString()
== ("Prepare to {rgb(255,0,0) DIE }{rgb(255,255,0) and} be"
@ "{rgb(255,255,255) reborn!}"));
}
protected static function SubTest_TextCreationFormattedComplex()
{
local string input, output;
local Text formatted;
Issue("`Text` object is not properly created from the formatted string.");
input = "This {$green i^4^5^cs q{#0f0f0f uit}^1e} a {rgb(45,234,154)"
@ "{$white ^bcomplex {$white ex}amp^ple}!}";
output = "This {rgb(0,128,0) i}{rgb(0,255,255) s q}{rgb(15,15,15) uit}"
$ "{rgb(255,0,0) e} a {rgb(0,0,255) complex }{rgb(255,255,255) ex}"
$ "{rgb(0,0,255) amp}{rgb(255,0,255) le}{rgb(45,234,154) !}";
formatted = class'Text'.static.ConstFromFormattedString(input);
TEST_ExpectNotNone(formatted);
TEST_ExpectTrue(formatted.ToFormattedString() == output);
}
protected static function Test_TextCopy()

Loading…
Cancel
Save