Added initial support for rendering composites

This is far from complete, but it's a start
and it will enable us to create things like tables
and other complex objects in the long run.
This commit is contained in:
Patrik Svensson
2020-07-29 17:34:54 +02:00
committed by Patrik Svensson
parent e596e6eb4f
commit 8e4f33bba4
41 changed files with 1164 additions and 24 deletions

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal sealed class MarkupBlockNode : IMarkupNode
{
private readonly List<IMarkupNode> _elements;
public MarkupBlockNode()
{
_elements = new List<IMarkupNode>();
}
public void Append(IMarkupNode element)
{
if (element != null)
{
_elements.Add(element);
}
}
public void Render(IAnsiConsole renderer)
{
foreach (var element in _elements)
{
element.Render(renderer);
}
}
}
}

View File

@ -0,0 +1,52 @@
using System;
namespace Spectre.Console.Internal
{
internal sealed class MarkupStyleNode : IMarkupNode
{
private readonly Styles? _style;
private readonly Color? _foreground;
private readonly Color? _background;
private readonly IMarkupNode _element;
public MarkupStyleNode(
Styles? style,
Color? foreground,
Color? background,
IMarkupNode element)
{
_style = style;
_foreground = foreground;
_background = background;
_element = element ?? throw new ArgumentNullException(nameof(element));
}
public void Render(IAnsiConsole renderer)
{
var style = (IDisposable)null;
var foreground = (IDisposable)null;
var background = (IDisposable)null;
if (_style != null)
{
style = renderer.PushStyle(_style.Value);
}
if (_foreground != null)
{
foreground = renderer.PushColor(_foreground.Value, foreground: true);
}
if (_background != null)
{
background = renderer.PushColor(_background.Value, foreground: false);
}
_element.Render(renderer);
background?.Dispose();
foreground?.Dispose();
style?.Dispose();
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace Spectre.Console.Internal
{
internal sealed class MarkupTextNode : IMarkupNode
{
public string Text { get; }
public MarkupTextNode(string text)
{
Text = text ?? throw new ArgumentNullException(nameof(text));
}
public void Render(IAnsiConsole renderer)
{
renderer.Write(Text);
}
}
}

View File

@ -0,0 +1,14 @@
namespace Spectre.Console.Internal
{
/// <summary>
/// Represents a parsed markup node.
/// </summary>
internal interface IMarkupNode
{
/// <summary>
/// Renders the node using the specified renderer.
/// </summary>
/// <param name="renderer">The renderer to use.</param>
void Render(IAnsiConsole renderer);
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal static class MarkupParser
{
public static IMarkupNode Parse(string text)
{
using var tokenizer = new MarkupTokenizer(text);
var root = new MarkupBlockNode();
var stack = new Stack<MarkupBlockNode>();
var current = root;
while (true)
{
var token = tokenizer.GetNext();
if (token == null)
{
break;
}
if (token.Kind == MarkupTokenKind.Text)
{
current.Append(new MarkupTextNode(token.Value));
continue;
}
else if (token.Kind == MarkupTokenKind.Open)
{
var (style, foreground, background) = MarkupStyleParser.Parse(token.Value);
var content = new MarkupBlockNode();
current.Append(new MarkupStyleNode(style, foreground, background, content));
current = content;
stack.Push(current);
continue;
}
else if (token.Kind == MarkupTokenKind.Close)
{
if (stack.Count == 0)
{
throw new InvalidOperationException($"Encountered closing tag when none was expected near position {token.Position}.");
}
stack.Pop();
if (stack.Count == 0)
{
current = root;
}
else
{
current = stack.Peek();
}
continue;
}
throw new InvalidOperationException("Encountered unkown markup token.");
}
if (stack.Count > 0)
{
throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?");
}
return root;
}
}
}

View File

@ -0,0 +1,65 @@
using System;
namespace Spectre.Console.Internal
{
internal static class MarkupStyleParser
{
public static (Styles? Style, Color? Foreground, Color? Background) Parse(string text)
{
var effectiveStyle = (Styles?)null;
var effectiveForeground = (Color?)null;
var effectiveBackground = (Color?)null;
var parts = text.Split(new[] { ' ' });
var foreground = true;
foreach (var part in parts)
{
if (part.Equals("on", StringComparison.OrdinalIgnoreCase))
{
foreground = false;
continue;
}
var style = Lookup.Instance.GetStyle(part);
if (style != null)
{
if (effectiveStyle == null)
{
effectiveStyle = Styles.None;
}
effectiveStyle |= style.Value;
}
else
{
var color = Lookup.Instance.GetColor(part);
if (color == null)
{
throw new InvalidOperationException("Could not find color..");
}
if (foreground)
{
if (effectiveForeground != null)
{
throw new InvalidOperationException("A foreground has already been set.");
}
effectiveForeground = color;
}
else
{
if (effectiveBackground != null)
{
throw new InvalidOperationException("A background has already been set.");
}
effectiveBackground = color;
}
}
}
return (effectiveStyle, effectiveForeground, effectiveBackground);
}
}
}

View File

@ -0,0 +1,18 @@
using System;
namespace Spectre.Console.Internal
{
internal sealed class MarkupToken
{
public MarkupTokenKind Kind { get; }
public string Value { get; }
public int Position { get; set; }
public MarkupToken(MarkupTokenKind kind, string value, int position)
{
Kind = kind;
Value = value ?? throw new ArgumentNullException(nameof(value));
Position = position;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Spectre.Console.Internal
{
internal enum MarkupTokenKind
{
Text = 0,
Open,
Close,
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Text;
namespace Spectre.Console.Internal
{
internal sealed class MarkupTokenizer : IDisposable
{
private readonly StringBuffer _reader;
public MarkupTokenizer(string text)
{
_reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text)));
}
public void Dispose()
{
_reader.Dispose();
}
public MarkupToken GetNext()
{
if (_reader.Eof)
{
return null;
}
var current = _reader.Peek();
if (current == '[')
{
var position = _reader.Position;
_reader.Read();
if (_reader.Eof)
{
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
current = _reader.Peek();
if (current == '[')
{
_reader.Read();
return new MarkupToken(MarkupTokenKind.Text, "[", position);
}
if (current == '/')
{
_reader.Read();
if (_reader.Eof)
{
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}.");
}
_reader.Read();
return new MarkupToken(MarkupTokenKind.Close, string.Empty, position);
}
var builder = new StringBuilder();
while (!_reader.Eof)
{
current = _reader.Peek();
if (current == ']')
{
break;
}
builder.Append(_reader.Read());
}
if (_reader.Eof)
{
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
_reader.Read();
return new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position);
}
else
{
var position = _reader.Position;
var builder = new StringBuilder();
while (!_reader.Eof)
{
current = _reader.Peek();
if (current == '[')
{
break;
}
builder.Append(_reader.Read());
}
return new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position);
}
}
}
}