diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index 6f7e44d..6ebbf5e 100644 --- a/sources/Text/MutableText.uc +++ b/sources/Text/MutableText.uc @@ -419,6 +419,44 @@ public final function MutableText Replace( return self; } +/** + * Changes formatting for characters with indices in range, specified as + * `[startIndex; startIndex + maxLength - 1]` to `newFormatting` parameter. + * + * 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 change formatting + * of. By default `0`, corresponding to the very first character. + * @param maxLength Max length of the segment to change formatting of. + * By default `0`, - that and all negative values are replaces by `MaxInt`, + * effectively extracting as much of a string as possible. + * @return Reference to the caller `MutableText` to allow for method chaining. + */ +public final function MutableText ChangeFormatting( + int startIndex, + int maxLength, + Formatting newFormatting) +{ + local int endIndex; + if (maxLength <= 0) return self; + if (startIndex >= GetLength()) return self; + + endIndex = Min(startIndex + maxLength, GetLength()) - 1; + startIndex = Max(startIndex, 0); + if (startIndex > endIndex) { + return self; + } + if (startIndex == 0 && endIndex == GetLength() - 1) { + ReformatWhole(newFormatting); + } + else { + ReformatRange(startIndex, endIndex, newFormatting); + } + return self; +} + defaultproperties { } \ No newline at end of file diff --git a/sources/Text/Tests/TEST_Text.uc b/sources/Text/Tests/TEST_Text.uc index d86d40a..59b1b30 100644 --- a/sources/Text/Tests/TEST_Text.uc +++ b/sources/Text/Tests/TEST_Text.uc @@ -40,6 +40,7 @@ protected static function TESTS() Test_StartsEndsWith(); Test_IndexOf(); Test_Replace(); + Test_ChangeFormatting(); } protected static function Test_TextCreation() @@ -1044,6 +1045,74 @@ protected static function SubTest_ReplacePartFormatting() $ "{rgb(4,4,4) cccc}{rgb(76,52,160) aB}{rgb(4,4,4) cc}a")); } +protected static function Test_ChangeFormatting() +{ + Context("Testing `ChangeFormatting()` method."); + SubTest_ChangeFormattingRegular(); + SubTest_ChangeFormattingEdgeCases(); +} + +protected static function SubTest_ChangeFormattingRegular() +{ + local Text template; + local MutableText testText; + local Text.Formatting greenFormatting, defaultFormatting; + greenFormatting = __().text.FormattingFromColor(__().color.Lime); + Issue("Formatting is not changed correctly."); + template = __().text.FromFormattedString( + "Normal part, {#ff0000 red part}, {#00ff00 green part}!!!"); + testText = template.MutableCopy().ChangeFormatting(3, 4, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + ("Nor{rgb(0,255,0) mal }part, {rgb(255,0,0) red part}, {rgb(0,255,0)" + @ "green part}!!!")); + testText = template.MutableCopy().ChangeFormatting(12, 10, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part,{rgb(0,255,0) red part,} {rgb(0,255,0) green part}!!!"); + testText = template.MutableCopy().ChangeFormatting(12, 11, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part,{rgb(0,255,0) red part, green part}!!!"); + // This test was added because it produced `none` access errors in the + // old implementation of `ChangeFormatting()` + testText = template.MutableCopy().ChangeFormatting(0, 35, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "{rgb(0,255,0) Normal part, red part, green part!!}!"); + testText = template.MutableCopy().ChangeFormatting(3, 4, defaultFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part, {rgb(255,0,0) red part}, {rgb(0,255,0) green part}!!!"); + testText = template.MutableCopy() + .ChangeFormatting(16, 13, defaultFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part, {rgb(255,0,0) red} part, green {rgb(0,255,0) part}!!!"); +} + +protected static function SubTest_ChangeFormattingEdgeCases() +{ + local Text template; + local MutableText testText; + local Text.Formatting greenFormatting, defaultFormatting; + greenFormatting = __().text.FormattingFromColor(__().color.Lime); + Issue("Formatting is not changed correctly when indices are out of or" + @ "near index boundaries."); + template = __().text.FromFormattedString( + "Normal part, {#ff0000 red part}, {#00ff00 green part}!!!"); + testText = template.MutableCopy().ChangeFormatting(33, 3, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part, {rgb(255,0,0) red part}, {rgb(0,255,0) green part!!!}"); + testText = template.MutableCopy().ChangeFormatting(36, 5, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part, {rgb(255,0,0) red part}, {rgb(0,255,0) green part}!!!"); + + testText = template.MutableCopy() + .ChangeFormatting(-10, 100, defaultFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + "Normal part, red part, green part!!!"); + testText = template.MutableCopy() + .ChangeFormatting(-10, 16, greenFormatting); + TEST_ExpectTrue(testText.ToFormattedString() == + ("{rgb(0,255,0) Normal} part, {rgb(255,0,0) red part}, {rgb(0,255,0)" + @ "green part}!!!")); +} + defaultproperties { caseName = "Text/MutableText" diff --git a/sources/Text/Text.uc b/sources/Text/Text.uc index f0d0507..28796c6 100644 --- a/sources/Text/Text.uc +++ b/sources/Text/Text.uc @@ -124,6 +124,90 @@ protected function Finalizer() 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 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`. * @@ -967,6 +1051,26 @@ private final function UpdateFormattingCacheFor(int index) } } +// 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.