RichText.razor
@using Sandbox
@using Sandbox.UI
@using System.Collections.Generic
@using System.Text.RegularExpressions
<root>
</root>
@code
{
public string Text { get; set; } = "";
protected override void OnParametersSet ()
{
base.OnParametersSet();
DeleteChildren( true );
var components = ParseText( Text );
foreach ( var (panelType, innerText) in components )
{
if ( panelType == typeof( Label ) )
{
var split = innerText.Split( " " );
foreach ( var s in split )
{
if(string.IsNullOrWhiteSpace(s))
{
continue;
}
AddChild( new Label( s ) );
}
}
else
{
var panel = TypeLibrary.Create<Panel>( panelType );
AddChild( panel );
if ( panel is IRichTextPanel richTextPanel )
{
richTextPanel.ParseRichText( innerText );
}
else
{
panel.AddChild( new Label( innerText ) );
}
}
}
}
/// <summary>
/// Parses rich text and returns a list of components to create
/// </summary>
/// <param name="text">The text to parse</param>
/// <returns>List of (ComponentType, InnerText) pairs where ComponentType is either a Panel type or typeof(Label)</returns>
public static List<(System.Type Type, string Text)> ParseText ( string text )
{
var result = new List<(System.Type Type, string Text)>();
if ( string.IsNullOrEmpty( text ) )
return result;
var allPanels = TypeLibrary.GetTypesWithAttribute<RichTextPanelAttribute>();
// Create a list of all matches with their positions
var matches = new List<(Match Match, System.Type PanelType, int Priority, string Pattern)>();
foreach ( var panelType in allPanels )
{
var pattern = panelType.Attribute.Pattern;
if ( string.IsNullOrEmpty( pattern ) )
continue;
var regex = new Regex( pattern );
var regexMatches = regex.Matches( text );
// Priority based on specificity - longer patterns get higher priority
// Also give higher priority to patterns with more complex regex features
var priority = pattern.Length;
if ( pattern.Contains( "(?<!" ) || pattern.Contains( "(?!" ) )
priority += 100; // Boost priority for lookahead/lookbehind patterns
foreach ( Match match in regexMatches )
{
matches.Add( (match, panelType.Type.TargetType, priority, pattern) );
}
}
// Sort matches by length (longer matches first for proper nesting),
// then by priority (higher priority first), then by position
matches.Sort( ( a, b ) =>
{
// First priority: longer matches first (this handles nesting properly)
var lengthComparison = b.Match.Length.CompareTo( a.Match.Length );
if ( lengthComparison != 0 )
return lengthComparison;
// Second priority: higher pattern priority first
var priorityComparison = b.Priority.CompareTo( a.Priority );
if ( priorityComparison != 0 )
return priorityComparison;
// Third priority: earlier position first
return a.Match.Index.CompareTo( b.Match.Index );
} );
// Remove overlapping matches (keep higher priority ones)
var finalMatches = new List<(Match Match, System.Type PanelType)>();
foreach ( var (match, panelType, priority, pattern) in matches )
{
var overlaps = false;
foreach ( var (existingMatch, _) in finalMatches )
{
if ( match.Index < existingMatch.Index + existingMatch.Length &&
match.Index + match.Length > existingMatch.Index )
{
overlaps = true;
break;
}
}
if ( !overlaps )
{
finalMatches.Add( (match, panelType) );
}
}
// Sort final matches by position
finalMatches.Sort( ( a, b ) => a.Match.Index.CompareTo( b.Match.Index ) );
// Build the component list by processing text segments and matches
var lastIndex = 0;
foreach ( var (match, panelType) in finalMatches )
{
// Add any text before this match as Label components
if ( match.Index > lastIndex )
{
var beforeText = text.Substring( lastIndex, match.Index - lastIndex );
if ( !string.IsNullOrEmpty( beforeText ) )
{
result.Add( (typeof( Label ), beforeText) );
}
}
// Add the panel component
// Use full match by default. Only use first capture group if it's clearly extracting content from wrapper syntax
var innerText = match.Value;
if ( match.Groups.Count > 1 && !string.IsNullOrEmpty( match.Groups[1].Value ) )
{
var captureGroup = match.Groups[1].Value;
var fullMatch = match.Value;
// Only use capture group if the full match starts and ends with wrapper characters
// that are NOT part of the capture group (indicating wrapper syntax like **text** or <i>text</i>)
var beforeCapture = fullMatch.Substring( 0, match.Groups[1].Index - match.Index );
var afterCapture = fullMatch.Substring( match.Groups[1].Index - match.Index + match.Groups[1].Length );
// Use capture group only if there are clear wrapper characters before AND after
if ( !string.IsNullOrEmpty( beforeCapture ) && !string.IsNullOrEmpty( afterCapture ) )
{
innerText = captureGroup;
}
}
result.Add( (panelType, innerText) );
lastIndex = match.Index + match.Length;
}
// Add any remaining text as a Label component
if ( lastIndex < text.Length )
{
var remainingText = text.Substring( lastIndex );
if ( !string.IsNullOrEmpty( remainingText ) )
{
result.Add( (typeof( Label ), remainingText) );
}
}
return result;
}
}