Editor/MarkupParser.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace XGUI.XGUIEditor
{
// Now parses Razor statements and code blocks into RazorBlock nodes
public static class SimpleMarkupParser
{
// Entry point: parses markup string into a tree of MarkupNode
public static List<MarkupNode> Parse( string input )
{
var nodes = new List<MarkupNode>();
int pos = 0;
while ( pos < input.Length )
{
SkipWhitespace( input, ref pos );
if ( pos >= input.Length ) break;
if ( input[pos] == '<' )
{
var node = ParseElement( input, ref pos );
if ( node != null ) nodes.Add( node );
}
else if ( input[pos] == '@' )
{
var razorNode = ParseRazorBlock( input, ref pos );
if ( razorNode != null ) nodes.Add( razorNode );
}
else
{
var text = ParseText( input, ref pos );
if ( !string.IsNullOrWhiteSpace( text ) )
nodes.Add( new MarkupNode { Type = NodeType.Text, TextContent = text } );
}
}
return nodes;
}
private static MarkupNode ParseElement( string input, ref int pos )
{
// Assumes input[pos] == '<'
pos++; // skip '<'
SkipWhitespace( input, ref pos );
// read tag names or attribute names
var tagName = ReadWhile( input, ref pos, c => char.IsLetterOrDigit( c ) || c == '-' || c == '_' || c == '@' );
if ( string.IsNullOrEmpty( tagName ) ) return null;
var node = new MarkupNode { Type = NodeType.Element, TagName = tagName };
// Read attributes
while ( true )
{
SkipWhitespace( input, ref pos );
if ( pos >= input.Length ) break;
if ( input[pos] == '/' || input[pos] == '>' ) break;
// Attribute name
var attrName = ReadWhile( input, ref pos, c => char.IsLetterOrDigit( c ) || c == '-' || c == '_' || c == '@' );
if ( string.IsNullOrEmpty( attrName ) ) break;
SkipWhitespace( input, ref pos );
string attrValue = null;
if ( pos < input.Length && input[pos] == '=' )
{
pos++; // skip '='
SkipWhitespace( input, ref pos );
attrValue = ReadAttributeValue( input, ref pos );
}
node.Attributes[attrName] = attrValue ?? "";
}
// Self-closing tag
if ( pos < input.Length - 1 && input[pos] == '/' && input[pos + 1] == '>' )
{
pos += 2;
return node;
}
// End of open tag
if ( pos < input.Length && input[pos] == '>' )
{
pos++;
// Parse children until </tag>
while ( true )
{
SkipWhitespace( input, ref pos );
if ( pos >= input.Length ) break;
if ( input[pos] == '<' && pos + 1 < input.Length && input[pos + 1] == '/' )
{
// End tag
pos += 2;
var endTag = ReadWhile( input, ref pos, c => char.IsLetterOrDigit( c ) || c == '-' || c == '_' );
SkipWhitespace( input, ref pos );
if ( pos < input.Length && input[pos] == '>' ) pos++;
break;
}
else if ( input[pos] == '<' )
{
var child = ParseElement( input, ref pos );
if ( child != null )
{
child.Parent = node;
node.Children.Add( child );
}
}
else if ( input[pos] == '@' )
{
var razorNode = ParseRazorBlock( input, ref pos );
if ( razorNode != null )
{
razorNode.Parent = node;
node.Children.Add( razorNode );
}
}
else
{
var text = ParseText( input, ref pos );
if ( !string.IsNullOrWhiteSpace( text ) )
node.Children.Add( new MarkupNode { Type = NodeType.Text, TextContent = text, Parent = node } );
}
}
}
return node;
}
// New method that captures entire Razor statements or code blocks as a single node
private static MarkupNode ParseRazorBlock( string input, ref int pos )
{
// Assumes input[pos] == '@'
int start = pos;
pos++; // skip '@'
var sb = new StringBuilder();
sb.Append( '@' );
// Capture any leading identifier (e.g. "foreach", "if", "code") plus spaces & parentheses
while ( pos < input.Length && input[pos] != '{' && input[pos] != '<' )
{
sb.Append( input[pos] );
pos++;
}
// If we encounter '{', capture until the matching '}' (preserving whitespace/newlines)
if ( pos < input.Length && input[pos] == '{' )
{
sb.Append( '{' );
pos++;
int braceCount = 1;
while ( pos < input.Length && braceCount > 0 )
{
if ( input[pos] == '{' ) braceCount++;
else if ( input[pos] == '}' ) braceCount--;
sb.Append( input[pos] );
pos++;
}
}
return new MarkupNode
{
Type = NodeType.RazorBlock,
TextContent = sb.ToString()
};
}
private static string ParseText( string input, ref int pos )
{
int start = pos;
while ( pos < input.Length && input[pos] != '<' && input[pos] != '@' )
pos++;
var str = input.Substring( start, pos - start );
return str.Trim();
}
private static string ReadWhile( string input, ref int pos, Func<char, bool> predicate )
{
int start = pos;
while ( pos < input.Length && predicate( input[pos] ) )
pos++;
return input.Substring( start, pos - start );
}
private static string ReadAttributeValue( string input, ref int pos )
{
if ( pos >= input.Length ) return "";
if ( input[pos] == '"' || input[pos] == '\'' )
{
char quote = input[pos++];
int start = pos;
while ( pos < input.Length && input[pos] != quote )
pos++;
var val = input.Substring( start, pos - start );
if ( pos < input.Length ) pos++; // skip closing quote
return val;
}
else
{
// Unquoted value
return ReadWhile( input, ref pos, c => !char.IsWhiteSpace( c ) && c != '>' && c != '/' );
}
}
private static void SkipWhitespace( string input, ref int pos )
{
while ( pos < input.Length && char.IsWhiteSpace( input[pos] ) )
pos++;
}
// Serialize tree back to markup with formatting
public static string Serialize( IEnumerable<MarkupNode> nodes )
{
var sb = new StringBuilder();
foreach ( var node in nodes )
SerializeNode( node, sb, 0 );
return sb.ToString();
}
private static void SerializeNode( MarkupNode node, StringBuilder sb, int indentLevel )
{
string indent = new string( '\t', indentLevel );
if ( node.Type == NodeType.Text )
{
// For text nodes, trim excess whitespace but preserve content
string trimmed = node.TextContent.Trim();
if ( !string.IsNullOrEmpty( trimmed ) )
{
sb.Append( indent );
sb.Append( trimmed );
sb.AppendLine();
}
}
else if ( node.Type == NodeType.Element )
{
// Start element tag with indentation
sb.Append( indent );
sb.Append( '<' ).Append( node.TagName );
// Add attributes
foreach ( var attr in node.Attributes )
{
sb.Append( ' ' ).Append( attr.Key );
if ( !string.IsNullOrEmpty( attr.Value ) )
sb.Append( "=\"" ).Append( attr.Value.Replace( "\"", """ ) ).Append( '"' );
}
if ( node.Children.Count == 0 )
{
// Self-closing tag
sb.AppendLine( " />" );
}
else
{
sb.AppendLine( ">" );
// Process children with increased indentation
foreach ( var child in node.Children )
SerializeNode( child, sb, indentLevel + 1 );
// Closing tag with proper indentation
sb.Append( indent );
sb.Append( "</" ).Append( node.TagName ).AppendLine( ">" );
}
}
else if ( node.Type == NodeType.RazorBlock )
{
// Just output the Razor code exactly
sb.Append( indent );
sb.AppendLine( node.TextContent );
}
}
public static Dictionary<string, string> ParseAttributes( string input )
{
var dict = new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
if ( string.IsNullOrWhiteSpace( input ) )
return dict;
// Regex: key="value", key='value', key=value, or key (valueless)
var regex = new System.Text.RegularExpressions.Regex(
@"(\w+)(?:\s*=\s*(?:""([^""]*)""|'([^']*)'|([^\s""']+)))?",
System.Text.RegularExpressions.RegexOptions.Compiled );
foreach ( System.Text.RegularExpressions.Match match in regex.Matches( input ) )
{
var key = match.Groups[1].Value;
var value = match.Groups[2].Success ? match.Groups[2].Value
: match.Groups[3].Success ? match.Groups[3].Value
: match.Groups[4].Success ? match.Groups[4].Value
: ""; // For valueless attributes like "checked"
dict[key] = value;
}
return dict;
}
}
}