RichTextBlock で遊んでみよう第三弾!
といいつつ、今回のメインは正規表現の話だったりします。
例えばゲーム内通貨名。世界観によって様々で、開発の途中で変わることもしばしばです。
この世界のお金は"<#Word:Money>"になります。
その度にテキストを全体置換するよりも、最初からメタデータで変数化できてるとよいですね。
例えばデコレータのタグ入力。
<ruby label="わがはい">吾輩>は\猫>である。
なんだか冗長に見えるこれより、
|吾輩"わがはい"は|猫"ねこ"<Important>である。
簡略的に分かりやすく入力したいですよね。今回はそんなお話です。
RichTextBlock テキストの置換処理
前々回の記事で拡張した、FMyDefaultRichTextMarkupParser に処理を加えます。
構文解析前の文字列を正規表現で直接修正します。
void FMyDefaultRichTextMarkupParser::Process(
TArray<FTextLineParseResults>& Results, const FString& Input, FString& Output )
{
// ============================ // ここから
FString ProcessInput( Input );
// 単語テキスト用タグの置換処理
ProcessReplaceString( ProcessInput );
// ルビ用タグの置換処理
ProcessReplaceRuby( ProcessInput );
// ============================ // ここまで
// ※以降は Input の参照を ProcessInput に変更する
// エンジン側実装の構文解析処理
TArray<FTextRange> LineRanges;
... // 省略
}
正規表現を思い出す
正規表現なんて年に1度使えばいいくらいの私なので、使うたびにググっていたのですが、今回とてもよい記事に出会えたのでメモしておきます。
正規表現を可視化してまとめたチートシート
正規表現の可視化するために利用しているサービスはregexper.comです。
各正規表現のチェックにはrubular.comなどを利用して確認する。
今まではプログラム実行して確かめていたのでw
イテレーションが非常に早くて感動しています。正規表現チョット楽しい!になった。
単語テキスト用タグの置換処理
この世界のお金は"<#Word:Money>"になります。
構文ルールは <#{ReplaceKey}:{ReplaceValue}> とします。
Key/Value でデータテーブルなどから単語を参照して置換する感じです。
void FMyDefaultRichTextMarkupParser::ProcessReplaceString(
FString& InOutString ) const
{
FRegexPattern RegexPattern( TEXT( "<#([\\w]+):([\\w]+)>" ) );
FRegexMatcher RegexMatcher( RegexPattern, InOutString );
struct FReplaceKeyValuePair
{
FString Key;
FString Value;
};
TMap<FTextRange, FReplaceKeyValuePair> ReplaceMap;
while ( RegexMatcher.FindNext() )
{
FReplaceKeyValuePair KVP;
KVP.Key = RegexMatcher.GetCaptureGroup( 1 );
KVP.Value = RegexMatcher.GetCaptureGroup( 2 );
FTextRange ElementRange(
RegexMatcher.GetMatchBeginning(), RegexMatcher.GetMatchEnding() );
ReplaceMap.Emplace( ElementRange, KVP );
}
TArray<FTextRange> Keys;
ReplaceMap.GetKeys( Keys );
// HITしたパターンを逆順にしてから
Algo::Reverse( Keys );
for ( const FTextRange& Key : Keys )
{
const FReplaceKeyValuePair& KVP = ReplaceMap[Key];
FString ReplacedString = ReplaceString( KVP.Key, KVP.Value );
if ( !ReplacedString.IsEmpty() )
{
// テキスト後方から置換していく
InOutString.RemoveAt( Key.BeginIndex, Key.EndIndex - Key.BeginIndex );
InOutString.InsertAt( Key.BeginIndex, ReplacedString );
}
}
}
FString FMyDefaultRichTextMarkupParser::ReplaceString(
const FString& ReplaceTag, const FString& ReplaceValue ) const
{
if ( ReplaceTag == TEXT( "Word" ) )
{
return // DataTableなどからReplaceValueを参照して取得する
}
return TEXT( "" );
}
ルビ用タグの置換処理
|吾輩"わがはい"は|猫"ねこ"<Important>である。
構文ルールは |{ContentString}"{RubyString}"<{TextStyleName}> とします。
小説家になろうのルビ仕様を参考にしました。
TextStyleName はオプションです。
※上記ページでは "|" 文字が使えなかったので "!" を使っています。
void FMyDefaultRichTextMarkupParser::ProcessReplaceRuby( FString& InOutString ) const
{
FRegexPattern RegexPattern( TEXT( "\\|([\\w]+)\"([\\w]+)\"(<([\\w]+)>)?" ) );
FRegexMatcher RegexMatcher( RegexPattern, InOutString );
struct FLabelWithRuby
{
FString Ruby;
FString Label;
FString Style;
};
TMap<FTextRange, FLabelWithRuby> ReplaceMap;
while ( RegexMatcher.FindNext() )
{
FLabelWithRuby LabelWithRuby;
LabelWithRuby.Label = RegexMatcher.GetCaptureGroup( 1 );
LabelWithRuby.Ruby = RegexMatcher.GetCaptureGroup( 2 );
LabelWithRuby.Style = RegexMatcher.GetCaptureGroup( 4 );
FTextRange ElementRange(
RegexMatcher.GetMatchBeginning(), RegexMatcher.GetMatchEnding() );
ReplaceMap.Emplace( ElementRange, LabelWithRuby );
}
TArray<FTextRange> Keys;
ReplaceMap.GetKeys( Keys );
Algo::Reverse( Keys );
for ( const FTextRange& Key : Keys )
{
InOutString.RemoveAt( Key.BeginIndex, Key.EndIndex - Key.BeginIndex );
const FLabelWithRuby& LabelWithRuby = ReplaceMap[Key];
// Attributeを配列に格納して、後でJoinする
TArray<FString> AttributeStrings =
{ FString::Printf( TEXT( "ruby=\"%s\"" ), *LabelWithRuby.Ruby ) };
if ( !LabelWithRuby.Style.IsEmpty() )
{
AttributeStrings.Emplace(
FString::Printf( TEXT( "style=\"%s\"" ), *LabelWithRuby.Style ) );
}
InOutString.InsertAt(
Key.BeginIndex, FString::Format( TEXT( "<ruby {0}>{1}</>" ), {
FString::Join( AttributeStrings, TEXT( "\u0020" ) ),
LabelWithRuby.Label,
} ) );
}
}