diff --git a/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs b/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs index 6ed39df..9afb92c 100644 --- a/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs +++ b/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs @@ -24,134 +24,154 @@ internal sealed class MarkupTokenizer : IDisposable } var current = _reader.Peek(); - if (current == '[') + return current == '[' ? ReadMarkup() : ReadText(); + } + + private bool ReadText() + { + var position = _reader.Position; + var builder = new StringBuilder(); + + var encounteredClosing = false; + while (!_reader.Eof) { - var position = _reader.Position; - - _reader.Read(); - - if (_reader.Eof) - { - throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); - } - - current = _reader.Peek(); + var current = _reader.Peek(); if (current == '[') { + // markup encountered. Stop processing. + break; + } + + // If we find a closing tag (']') there must be two of them. + if (current == ']') + { + if (encounteredClosing) + { + _reader.Read(); + encounteredClosing = false; + continue; + } + + encounteredClosing = true; + } + else + { + if (encounteredClosing) + { + throw new InvalidOperationException( + $"Encountered unescaped ']' token at position {_reader.Position}."); + } + } + + builder.Append(_reader.Read()); + } + + if (encounteredClosing) + { + throw new InvalidOperationException($"Encountered unescaped ']' token at position {_reader.Position}."); + } + + Current = new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position); + return true; + } + + private bool ReadMarkup() + { + var position = _reader.Position; + + _reader.Read(); + + if (_reader.Eof) + { + throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + } + + var current = _reader.Peek(); + switch (current) + { + case '[': + // No markup but instead escaped markup in text. _reader.Read(); Current = new MarkupToken(MarkupTokenKind.Text, "[", position); return true; - } - - if (current == '/') - { + case '/': + // Markup closed. _reader.Read(); if (_reader.Eof) { - throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + throw new InvalidOperationException( + $"Encountered malformed markup tag at position {_reader.Position}."); } current = _reader.Peek(); if (current != ']') { - throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + throw new InvalidOperationException( + $"Encountered malformed markup tag at position {_reader.Position}."); } _reader.Read(); Current = new MarkupToken(MarkupTokenKind.Close, string.Empty, position); return true; - } + } - var builder = new StringBuilder(); - while (!_reader.Eof) + // Read the "content" of the markup until we find the end-of-markup + var builder = new StringBuilder(); + var encounteredOpening = false; + var encounteredClosing = false; + while (!_reader.Eof) + { + current = _reader.Peek(); + + if (current == ']' && !encounteredOpening) { - current = _reader.Read(); - var next = '\0'; - if (!_reader.Eof) - { - next = _reader.Peek(); - } - - if (current == ']') - { - if (next != ']') - { - break; - } - - _reader.Read(); - } - - builder.Append(current); - - if (current != '[') + if (encounteredClosing) { + builder.Append(_reader.Read()); + encounteredClosing = false; continue; } - if (next == '[') - { - _reader.Read(); - } - else - { - throw new InvalidOperationException( - $"Encountered malformed markup tag at position {_reader.Position - 1}."); - } + _reader.Read(); + encounteredClosing = true; + continue; } - if (_reader.Eof) + if (current == '[' && !encounteredClosing) { - throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); - } - - Current = new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position); - return true; - } - else - { - var position = _reader.Position; - var builder = new StringBuilder(); - - var encounteredClosing = false; - while (!_reader.Eof) - { - current = _reader.Peek(); - if (current == '[') + if (encounteredOpening) { - break; - } - else if (current == ']') - { - if (encounteredClosing) - { - _reader.Read(); - encounteredClosing = false; - continue; - } - - encounteredClosing = true; - } - else - { - if (encounteredClosing) - { - throw new InvalidOperationException( - $"Encountered unescaped ']' token at position {_reader.Position}."); - } + builder.Append(_reader.Read()); + encounteredOpening = false; + continue; } - builder.Append(_reader.Read()); + _reader.Read(); + encounteredOpening = true; + continue; } if (encounteredClosing) { - throw new InvalidOperationException($"Encountered unescaped ']' token at position {_reader.Position}."); + break; } - Current = new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position); - return true; + if (encounteredOpening) + { + throw new InvalidOperationException( + $"Encountered malformed markup tag at position {_reader.Position - 1}."); + } + + builder.Append(_reader.Read()); } + + if (_reader.Eof) + { + throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}."); + } + + Current = new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position); + return true; } } \ No newline at end of file diff --git a/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs index 58d0a90..c00bde3 100644 --- a/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs +++ b/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs @@ -141,5 +141,18 @@ public partial class AnsiConsoleTests result.ShouldBeOfType() .Message.ShouldBe("Encountered closing tag when none was expected near position 5."); } + + [Fact] + public void Should_Not_Get_Confused_When_Mixing_Escaped_And_Unescaped() + { + // Given + var console = new TestConsole(); + + // When + console.Markup("[grey][[grey]][/][white][[white]][/]"); + + // Then + console.Output.ShouldBe("[grey][white]"); + } } }