Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Docs/English/CoreFeatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ When creating any kind of hint, the properties listed below are always available
| Content | `AbstractHintContent` | The content provider for this hint. Default: `StringContent("")` |
| Text | `string?` | Shortcut to get/set static text. Setting this replaces `Content` with a new `StringContent` |
| AutoText | `AutoContent.TextUpdateHandler?` | Shortcut to get/set a dynamic text delegate. Setting this replaces `Content` with a new `AutoContent` |
| PreserveCase | `bool` | Preserve mixed-case text instead of the game's default small-caps hint rendering. Default: `false` |
| Hide | `bool` | Whether the hint is hidden. Default: `false` |

**Usage Example:**
Expand All @@ -60,9 +61,12 @@ When creating any kind of hint, the properties listed below are always available
// Properties auto-sync to the player's screen
hint.Text = "Updated text";
hint.FontSize = 30;
hint.PreserveCase = true;
// No additional method calls needed
```

`PreserveCase` only changes the text sent for measurement and rendering; it does not mutate `Text` or `Content`. Existing `<lowercase>`, `<uppercase>`, `<allcaps>`, and `<smallcaps>` regions remain authoritative. Content inside `<noparse>` is not rewritten because injected tags would be displayed literally there.

---

### Hint
Expand Down Expand Up @@ -481,4 +485,4 @@ hint.AutoText = (ev) =>
};
```

Click [here](/Docs/English/README.md) to go back to read me
Click [here](/Docs/English/README.md) to go back to read me
4 changes: 4 additions & 0 deletions Docs/SimplifiedChinese/CoreFeatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
| Content | `AbstractHintContent` | 此提示的内容提供者。默认值:`StringContent("")` |
| Text | `string?` | 获取/设置静态文本的快捷方式。设置此属性会用新的 `StringContent` 替换 `Content` |
| AutoText | `AutoContent.TextUpdateHandler?` | 获取/设置动态文本委托的快捷方式。设置此属性会用新的 `AutoContent` 替换 `Content` |
| PreserveCase | `bool` | 保留大小写混排文本,而不使用游戏提示默认的小型大写字母渲染。默认值:`false` |
| Hide | `bool` | 提示是否隐藏。默认值:`false` |

**使用示例:**
Expand All @@ -60,9 +61,12 @@
// 属性自动同步到玩家屏幕
hint.Text = "更新后的文字";
hint.FontSize = 30;
hint.PreserveCase = true;
// 无需额外的方法调用
```

`PreserveCase` 只会更改用于测量和渲染的文本,不会修改 `Text` 或 `Content`。已有的 `<lowercase>`、`<uppercase>`、`<allcaps>` 和 `<smallcaps>` 区域仍然优先。`<noparse>` 内的内容不会被改写,因为插入的标签会在该区域内显示为普通文本。

---

### Hint
Expand Down
20 changes: 19 additions & 1 deletion HintServiceMeow.Tests/Core/Models/HintTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public void Constructor_WhenDefault_HasExpectedDefaults()
Assert.AreEqual(HintAlignment.Center, hint.Alignment);
Assert.AreEqual(HintVerticalAlign.Middle, hint.YCoordinateAlign);
Assert.AreEqual(20, hint.FontSize);
Assert.IsFalse(hint.PreserveCase);
}

[TestMethod]
Expand All @@ -35,6 +36,7 @@ public void CopyConstructor_WhenSourceProvided_ClonesMutableStateButUsesNewGuid(
YCoordinate = 34,
Alignment = HintAlignment.Left,
YCoordinateAlign = HintVerticalAlign.Top,
PreserveCase = true,
Text = "text"
};

Expand All @@ -47,6 +49,7 @@ public void CopyConstructor_WhenSourceProvided_ClonesMutableStateButUsesNewGuid(
Assert.AreEqual(source.YCoordinate, copy.YCoordinate);
Assert.AreEqual(source.Alignment, copy.Alignment);
Assert.AreEqual(source.Text, copy.Text);
Assert.AreEqual(source.PreserveCase, copy.PreserveCase);
}

[TestMethod]
Expand All @@ -62,11 +65,13 @@ public void PropertySetters_WhenValueChanges_NotifiesAndInvokesAnalyser()
// Act
hint.XCoordinate = 10;
hint.YCoordinate = 20;
hint.PreserveCase = true;

// Assert
CollectionAssert.Contains(props, "XCoordinate");
CollectionAssert.Contains(props, "YCoordinate");
Assert.AreEqual(2, analyser.OnUpdateCallCount);
CollectionAssert.Contains(props, "PreserveCase");
Assert.AreEqual(3, analyser.OnUpdateCallCount);
}

[TestMethod]
Expand Down Expand Up @@ -113,4 +118,17 @@ public void ContentReplacement_WhenNewContentSet_UnsubscribesOldAndSubscribesNew
// Assert
Assert.AreEqual(2, changed); // set + new content update only
}

[TestMethod]
public void ResetFields_WhenHintReturnsToPool_ClearsPreserveCase()
{
// Arrange
Hint hint = new() { PreserveCase = true };

// Act
hint.ResetFields();

// Assert
Assert.IsFalse(hint.PreserveCase);
}
}
21 changes: 21 additions & 0 deletions HintServiceMeow.Tests/Core/Utilities/Parser/HintParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ public void ParseToMessage_WhenIllegalTagsPresent_RemovesThem()
StringAssert.Contains(message, "Y");
}

[TestMethod]
public void ParseToMessage_WhenPreserveCaseChanges_KeepsLegacyDefaultAndSeparatesCachedOutput()
{
// Arrange
HintCollection collection = new();
Hint hint = new() { Text = "Hello" };
collection.AddHint("a", hint);

HintParser parser = new();

// Act
string legacyMessage = parser.ParseToMessage(collection);
hint.PreserveCase = true;
string preservedMessage = parser.ParseToMessage(collection);

// Assert
StringAssert.Contains(legacyMessage, "Hello");
Assert.IsFalse(legacyMessage.Contains("<lowercase>"));
StringAssert.Contains(preservedMessage, "H<lowercase>ello</lowercase>");
}

[TestMethod]
public void ParseToMessage_WhenMultipleHints_SortsByVisualBottomPosition()
{
Expand Down
170 changes: 170 additions & 0 deletions HintServiceMeow.Tests/Core/Utilities/Parser/HintTextFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using HintServiceMeow.Core.Models.Arguments;
using HintServiceMeow.Core.Models.HintContent;
using HintServiceMeow.Core.Models.Hints;
using HintServiceMeow.Core.Utilities.Parser;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace HintServiceMeow.Tests.Core.Utilities.Parser;

[TestClass]
public class HintTextFormatterTests
{
[TestMethod]
public void GetText_WhenPreserveCaseIsDisabled_ReturnsOriginalContent()
{
// Arrange
string text = new("Hello".ToCharArray());
Hint hint = new() { Text = text };

// Act
string result = HintTextFormatter.GetText(hint);

// Assert
Assert.AreSame(text, result);
}

[TestMethod]
public void PreserveCase_WhenMixedCase_WrapsOnlyLowercaseSpans()
{
// Act
string result = HintTextFormatter.PreserveCase("Hello WORLD - camelCase 123");

// Assert
Assert.AreEqual(
"H<lowercase>ello </lowercase>WORLD - <lowercase>camel</lowercase>C<lowercase>ase 123</lowercase>",
result);
}

[TestMethod]
public void PreserveCase_WhenRichTextIsPresent_LeavesTagsUnchanged()
{
// Act
string result = HintTextFormatter.PreserveCase("<color=#fff>Hello</color>");

// Assert
Assert.AreEqual("<color=#fff>H<lowercase>ello</lowercase></color>", result);
}

[TestMethod]
public void PreserveCase_WhenExplicitCaseTagsArePresent_LeavesTheirContentAuthoritative()
{
// Arrange
string text = "<uppercase>Do not change</uppercase> then <smallcaps>Keep me</smallcaps>";

// Act
string result = HintTextFormatter.PreserveCase(text);

// Assert
Assert.AreEqual(
"<uppercase>Do not change</uppercase> <lowercase>then </lowercase><smallcaps>Keep me</smallcaps>",
result);
}

[TestMethod]
public void PreserveCase_WhenExplicitCaseTagsAreNested_ResumesAfterTheOuterScopeCloses()
{
// Arrange
string text = "pre <uppercase>A <lowercase>Bc</lowercase> d</uppercase> post";

// Act
string result = HintTextFormatter.PreserveCase(text);

// Assert
Assert.AreEqual(
"<lowercase>pre </lowercase><uppercase>A <lowercase>Bc</lowercase> d</uppercase> <lowercase>post</lowercase>",
result);
}

[TestMethod]
public void PreserveCase_WhenNoParseIsPresent_DoesNotInjectMarkupIntoLiteralContent()
{
// Arrange
string text = "before <noparse><b>literal lower</b></noparse> after";

// Act
string result = HintTextFormatter.PreserveCase(text);

// Assert
Assert.AreEqual(
"<lowercase>before </lowercase><noparse><b>literal lower</b></noparse> <lowercase>after</lowercase>",
result);
}

[TestMethod]
public void PreserveCase_WhenUnicodeTextIsPresent_PreservesCasedLettersAndLeavesCjkUnchanged()
{
// Act
string result = HintTextFormatter.PreserveCase("École 中文 Привет");

// Assert
Assert.AreEqual(
"É<lowercase>cole 中文 </lowercase>П<lowercase>ривет</lowercase>",
result);
}

[TestMethod]
public void PreserveCase_WhenNothingNeedsChanging_ReturnsOriginalInstance()
{
// Arrange
string text = new("ABC 中文 123".ToCharArray());

// Act
string result = HintTextFormatter.PreserveCase(text);

// Assert
Assert.AreSame(text, result);
}

[TestMethod]
public void PreserveCase_WhenSelfClosingCaseTagIsPresent_DoesNotTreatItAsAnOpenScope()
{
// Act
string result = HintTextFormatter.PreserveCase("<lowercase/>after");

// Assert
Assert.AreEqual("<lowercase/><lowercase>after</lowercase>", result);
}

[TestMethod]
public void GetText_WhenAutoContentIsUsed_FormatsItsCurrentValueWithoutMutatingIt()
{
// Arrange
AutoContent content = new(_ => "Hello");
content.TryUpdate(new ContentUpdateArg(null!, null!));
Hint hint = new() { Content = content, PreserveCase = true };

// Act
string result = HintTextFormatter.GetText(hint);

// Assert
Assert.AreEqual("H<lowercase>ello</lowercase>", result);
Assert.AreEqual("Hello", content.GetText());
}

[TestMethod]
public void PreserveCase_WhenTransformedTextRepeats_ReusesCachedResult()
{
// Arrange
string firstInput = new("cache me uniquely".ToCharArray());
string secondInput = new("cache me uniquely".ToCharArray());

// Act
string firstResult = HintTextFormatter.PreserveCase(firstInput);
string secondResult = HintTextFormatter.PreserveCase(secondInput);

// Assert
Assert.AreSame(firstResult, secondResult);
}

[TestMethod]
public void PreserveCase_WhenUnknownAngleBracketTokenIsPresent_CopiesItUnchanged()
{
// Act
string result = HintTextFormatter.PreserveCase("before <player lowercase> after");

// Assert
Assert.AreEqual(
"<lowercase>before </lowercase><player lowercase> <lowercase>after</lowercase>",
result);
}
}
15 changes: 15 additions & 0 deletions HintServiceMeow.Tests/Core/Utilities/Tools/CoordinateToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ public void GetTextHeight_WhenLineHeightIsNegative_Throws()
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => tools.GetTextHeight("x", 20, -1));
}

[TestMethod]
public void GetTextHeight_WhenPreserveCaseIsEnabled_MeasuresTransformedText()
{
// Arrange
CoordinateTools tools = new();
Hint hint = new() { Text = "a", FontSize = 20, PreserveCase = true };

// Act
float hintHeight = tools.GetTextHeight(hint);
float explicitlyLowercaseHeight = tools.GetTextHeight("<lowercase>a</lowercase>", 20, 0);

// Assert
Assert.AreEqual(explicitlyLowercaseHeight, hintHeight);
}

[TestMethod]
public void GetLineInfos_WhenParsingCompletes_ReturnsParserToPool()
{
Expand Down
Loading