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; 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> stackCommands;
// 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`.
var array<Formatting> formattingStack; var array<Formatting> formattingStack;
/** /**
* Clears all current data from the caller `MutableText` instance. * Clears all current data from the caller `MutableText` instance.
@ -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`.
* *
@ -239,69 +279,99 @@ public final function MutableText AppendColoredString(
public final function MutableText AppendFormattedString( public final function MutableText AppendFormattedString(
string source, string source,
optional Formatting defaultFormatting) 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 int i;
local Parser parser; local Parser tagParser;
SplitFormattedStringIntoBlocks(source); BuildFormattingStackCommands(sourceParser);
if (splitBlocks.length <= 0) { 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(stackCommands[i].tag);
SetFormatting(PushIntoFormattingStack(parser)); SetFormatting(PushIntoFormattingStack(tagParser));
} }
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 FormattedStackCommand nextCommand;
local FormattedBlock nextBlock; stackCommands.length = 0;
splitBlocks.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 || splitBlocks.length == 0) { if (nextCommand.contents.length > 0 || stackCommands.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, on order. // sequence) defined in formatted string, in 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;
} }
/** /**
@ -477,5 +560,6 @@ public final function MutableText ChangeFormatting(
defaultproperties 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() 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 string plainString, coloredString, formattedString;
local Text plain, colored, formatted; local Text plain, colored, formatted;
Context("Testing basic functionality for creating `Text` objects.");
Issue("`Text` object is not properly created from the plain string."); Issue("`Text` object is not properly created from the plain string.");
plainString = "Prepare to DIE and be reborn!"; plainString = "Prepare to DIE and be reborn!";
plain = class'Text'.static.ConstFromPlainString(plainString); plain = class'Text'.static.ConstFromPlainString(plainString);
@ -62,10 +68,27 @@ protected static function Test_TextCreation()
TEST_ExpectTrue(colored.ToColoredString() == coloredString); TEST_ExpectTrue(colored.ToColoredString() == coloredString);
Issue("`Text` object is not properly created from the formatted string."); 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); formatted = class'Text'.static.ConstFromFormattedString(formattedString);
TEST_ExpectNotNone(formatted); 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() protected static function Test_TextCopy()

Loading…
Cancel
Save