[UE4] RichTextBlock で独自タグを定義する

[RichTextBlock](https://www.unrealengine.com/ja/tech-blog/advanced-text-styling-with-rich-text-block) で遊んでみよう第三弾！
といいつつ、今回のメインは正規表現の話だったりします。
![27b6c927cec57ee0fcef60e7ae0aa8bc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/102eba09-5ded-a405-5af2-6def06159499.png)
例えばゲーム内通貨名。[世界観によって様々](https://dic.nicovideo.jp/a/%E6%9E%B6%E7%A9%BA%E3%81%AE%E9%80%9A%E8%B2%A8%E4%B8%80%E8%A6%A7)で、開発の途中で変わることもしばしばです。

> この世界のお金は"<#Word:Money>"になります。

その度にテキストを全体置換するよりも、最初からメタデータで変数化できてるとよいですね。
![ae6d5644761ad687e9d5214bd11a6001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/ea6a4050-7baf-9ef9-a13f-85dda34d7e5e.png)
例えばデコレータのタグ入力。

> \<ruby label="わがはい">吾輩</>は\<ruby label="ねこ" style="Important">猫</>である。

なんだか冗長に見えるこれより、

> |吾輩"わがはい"は|猫"ねこ"\<Important>である。

簡略的に分かりやすく入力したいですよね。今回はそんなお話です。

# RichTextBlock テキストの置換処理
[前々回の記事](https://qiita.com/koorinonaka/items/5eb82738479667fa3d9c#%E6%A7%8B%E6%96%87%E8%A7%A3%E6%9E%90%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E6%8B%A1%E5%BC%B5%E3%81%99%E3%82%8B)で拡張した、FMyDefaultRichTextMarkupParser に処理を加えます。
構文解析前の文字列を正規表現で直接修正します。

```C++:FMyDefaultRichTextMarkupParser.cpp
void FMyDefaultRichTextMarkupParser::Process(
	TArray<FTextLineParseResults>& Results, const FString& Input, FString& Output )
{
// ============================ // ここから
	FString ProcessInput( Input );

	// 単語テキスト用タグの置換処理
	ProcessReplaceString( ProcessInput );

	// ルビ用タグの置換処理
	ProcessReplaceRuby( ProcessInput );
// ============================ // ここまで
	// ※以降は Input の参照を ProcessInput に変更する
	// エンジン側実装の構文解析処理
	TArray<FTextRange> LineRanges;
	... // 省略
}
```

## 正規表現を思い出す
正規表現なんて年に1度使えばいいくらいの私なので、使うたびにググっていたのですが、今回とてもよい記事に出会えたのでメモしておきます。
[正規表現を可視化してまとめたチートシート](https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128)

> 正規表現の可視化するために利用しているサービスは[regexper.com](https://regexper.com/)です。
> 各正規表現のチェックには[rubular.com](https://rubular.com)などを利用して確認する。

今まではプログラム実行して確かめていたのでｗ
イテレーションが非常に早くて感動しています。正規表現チョット楽しい！になった。

## 単語テキスト用タグの置換処理

> この世界のお金は"<#Word:Money>"になります。

構文ルールは **<#{ReplaceKey}:{ReplaceValue}>** とします。
![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/053767b9-a3b8-ec46-f0c1-92bbf3e379d7.png)
Key/Value でデータテーブルなどから単語を参照して置換する感じです。

```C++:FMyDefaultRichTextMarkupParser.cpp
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}>** とします。
[小説家になろう](https://syosetu.com/man/ruby/)のルビ仕様を参考にしました。
TextStyleName はオプションです。
![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/b9246be5-552b-cc8d-9d1f-d97251f98ca2.png)
![0e373075b190c2fcaf7fe9bfd82000fb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/c1ae7cda-6b28-a388-746c-b9cef84742c6.png)
※上記ページでは "|" 文字が使えなかったので "!" を使っています。

```C++:FMyDefaultRichTextMarkupParser.cpp
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,
			} ) );
	}
}
```

# RichTextBlock 完全に理解した
![download.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/76ccb08d-018a-c4f8-b0f7-3a51ec6f2844.jpeg)
だいたいやりたいことはやったかな？
構文ネストだけが心残りです。ぐぬぬ・・・。