Search code examples
c#.netalgorithmwinui-3text-formatting

Recognizing tags in the Text property of a TextBlock element in a WinUI 3 application


I know that the text of the TextBlock element can be formatted (italic, bold, underlined, etc.) using both C# code and XAML markup, which is very convenient. Here is an example from the MSDN site.

<TextBlock FontFamily="Arial">
    <Run Foreground="Blue" FontWeight="Light" Text="This text demonstrates "></Run>
    <Span FontWeight="SemiBold">
        <Run FontStyle="Italic">the use of inlines </Run>
        <Run Foreground="Red">with formatting.</Run>
    </Span>
</TextBlock>

But the problem is that I can't do it in the XAML markup, because the text will be updated on button click and there is a lot of it. I have to do this in C# code, but again I can't manually tweak each line because there is really, really a lot of text. I have it prepared in advance and tags are already written there in this style:

"WinUI 3 {b}decouples{/b} WinRT XAML from the {i}operating system{/i} as a separate..."

Previously, I used this text in RenPy and there these tags were processed by the engine. I'd like to achieve the same in WinUI 3. It's not difficult for me to change the tags themselves to some other ones, but in any case, I would like to achieve the effect that these tags are parsed on the fly and the text is modified based on these tags. Is it possible to somehow implement such behavior? Or maybe there are some alternatives? Please tell me what can be done about it.

Of course, the idea comes to my mind to write some algorithm that will parse tags and wrap their content in the necessary classes like Bold, Italic, Underline, etc. But how to implement it, I have no idea

UPDATE

Here is my final solution to this problem. Many thanks to Andrew KeepCoding for the tip

private List<string> Parsing(string text)
{
    List<string> tokens = new List<string>();
    Regex regex = new Regex(@"{[ibsu]}|{\/[ibsu]}");
    MatchCollection matches = regex.Matches(text);
    int lastIndex = 0;

    foreach (Match match in matches.Cast<Match>())
    {
        if (match.Index > lastIndex)
        {
            tokens.Add(text[lastIndex..match.Index]);
        }

        tokens.Add(match.Value);
        lastIndex = match.Index + match.Length;
    }

    if (lastIndex < text.Length)
    {
        tokens.Add(text[lastIndex..]);
    }

    return tokens;
}
private void SetText(string text)
{
    Stack<Span> stack = new Stack<Span>();
    stack.Push(new Span());

    foreach (string token in Parsing(text))
    {
        switch (token)
        {
            case "{b}":
                stack.Push(new Bold());
                continue;
            case "{i}":
                stack.Push(new Italic());
                continue;
            case "{u}":
                stack.Push(new Underline());
                continue;
            case "{s}":
                stack.Push(new Span() { TextDecorations = TextDecorations.Strikethrough });
                continue;
            case "{/b}":
            case "{/i}":
            case "{/u}":
            case "{/s}":
                Span span = stack.Pop();
                stack.Peek().Inlines.Add(span);
                continue;
            default:
                break;
        }

        Run run = new Run() { Text = token };
        stack.Peek().Inlines.Add(run);
    }

    _TextBlock.Inlines.Add(stack.Pop());
}

Solution

  • You could use the RichTextBlock. Something like this should work:

    <Grid ColumnDefinitions="*,*">
        <TextBox
            Grid.Column="0"
            VerticalAlignment="Top"
            Text="WinUI 3 {b}decouples{/b} WinRT XAML from the {i}operating system{/i} as a separate..."
            TextChanged="InputTextBox_TextChanged" />
        <RichTextBlock
            x:Name="OutputRichTextBlock"
            Grid.Column="1" />
    </Grid>
    
    private static IEnumerable<string> TokenizeString(string input)
    {
        List<string> tokens = new();
        string pattern = @"(\{[^{}]*\})";
        Regex regex = new(pattern);
        MatchCollection matches = regex.Matches(input);
        int lastIndex = 0;
    
        foreach (Match match in matches.Cast<Match>())
        {
            if (match.Index > lastIndex)
            {
                tokens.Add(input[lastIndex..match.Index]);
            }
    
            tokens.Add(match.Value);
            lastIndex = match.Index + match.Length;
        }
    
        if (lastIndex < input.Length)
        {
            tokens.Add(input[lastIndex..]);
        }
    
        return tokens;
    }
    
    private void InputTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (sender is not TextBox senderTextBox)
        {
            return;
        }
    
        FontWeight fontWeight = FontWeights.Normal;
        FontStyle fontStyle = FontStyle.Normal;
    
        Paragraph paragraph = new();
    
        foreach (string token in TokenizeString(senderTextBox.Text))
        {
            if (token == "{b}" || token == "{/b}")
            {
                fontWeight = token == "{b}" ? FontWeights.Bold : FontWeights.Normal;
                continue;
            }
    
            if (token == "{i}" || token == "{/i}")
            {
                fontStyle = token == "{i}" ? FontStyle.Italic : FontStyle.Normal;
                continue;
            }
    
            Run run = new()
            {
                Text = token,
                FontWeight = fontWeight,
                FontStyle = fontStyle,
            };
    
            paragraph.Inlines.Add(run);
        }
    
        this.OutputRichTextBlock.Blocks.Add(paragraph);
    }