xn/Parser/XnParser.cs
Dominic Höglinger 6f94002cf4 Fix comment handling
Clanker: claude-sonnet_v4.6
2026-04-24 20:11:08 +02:00

251 lines
11 KiB
C#

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Sprache;
using System.Globalization;
using XNeedle.Parser.Elements;
using Part = XNeedle.Parser.Elements.Part;
using NullPart = XNeedle.Parser.Elements.NullPart;
using XNeedle.Transformation;
using Rule = XNeedle.Transformation.Rule;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace XNeedle.Parser
{
public class XnParser
{
static readonly ParameterExpression CtxParam = Expression.Parameter(typeof(Context), "ctx");
static Parser<ExpressionType> Operator(string op, ExpressionType opType)
{
return Parse.String(op).Token().Return(opType);
}
//static MethodInfo ConcatMethod = typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) });
static readonly Parser<ExpressionType> Add = Operator("+", ExpressionType.AddChecked);
static readonly Parser<ExpressionType> Subtract = Operator("-", ExpressionType.SubtractChecked);
static readonly Parser<ExpressionType> Multiply = Operator("*", ExpressionType.MultiplyChecked);
static readonly Parser<ExpressionType> Divide = Operator("/", ExpressionType.Divide);
static readonly Parser<ExpressionType> Modulo = Operator("%", ExpressionType.Modulo);
static readonly Parser<ExpressionType> Power = Operator("^", ExpressionType.Power);
static Parser<Expression> Function(Context ctx)
{
return
from name in Parse.Letter.AtLeastOnce().Text()
from lparen in Parse.Char('(')
from expr in Parse.Ref(() => Expr(ctx)).DelimitedBy(Parse.Char(',').Token()).Optional()
from rparen in Parse.Char(')')
select CallFunction(ctx, name, expr.GetOrElse(new Expression[0]).ToArray());
}
static Expression CallFunction(Context ctx, string name, Expression[] parameters)
{
var mathMethodInfo = typeof(Math).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).ToArray());
if (mathMethodInfo != null)
{
return Expression.Call(mathMethodInfo, parameters);
}
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).Prepend(typeof(Context)).ToArray());
if (ctxMethodInfo != null)
{
return Expression.Call(ctxMethodInfo, parameters.Prepend(CtxParam).ToArray());
}
throw new ParseException(string.Format("Function '{0}({1})' does not exist.", name,
string.Join(",", parameters.Select(e => e.Type.Name))));
}
static readonly Parser<Expression> NumberConstant =
Parse.Decimal
.Select(x => Expression.Constant(double.Parse(x)))
.Named("number");
private static readonly Parser<Expression> StringConstant =
from open in Parse.Char('"')
from value in Parse.CharExcept('"').Many().Text()
from close in Parse.Char('"')
select Expression.Constant(new string(value));
static Parser<Expression> Factor(Context ctx)
{
return (from lparen in Parse.Char('(')
from expr in Parse.Ref(() => Expr(ctx))
from rparen in Parse.Char(')')
select expr).Named("expression")
.XOr(NumberConstant)
.XOr(StringConstant)
.XOr(Function(ctx));
}
static Parser<Expression> Operand(Context ctx)
{
return ((from sign in Parse.Char('-')
from factor in Factor(ctx)
select Expression.Negate(factor)
).XOr(Factor(ctx))).Token();
}
static Parser<Expression> InnerTerm(Context ctx)
{
return Parse.ChainRightOperator(Power, Operand(ctx), Expression.MakeBinary);
}
static Parser<Expression> Term(Context ctx)
{
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo), InnerTerm(ctx), Expression.MakeBinary);
}
static Parser<Expression> ConcatExpr(Context ctx)
{
var concatMethod = typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) });
return
from left in StringConstant.Or(Function(ctx))
from op in Parse.String("..").Token()
from right in StringConstant.Or(Function(ctx))
select Expression.Add(left, right, concatMethod);
}
static Parser<Expression> ArithExpr(Context ctx)
{
return Parse.ChainOperator(Add.Or(Subtract), Term(ctx), Expression.MakeBinary);
}
static Parser<Expression> Expr(Context ctx)
{
return ConcatExpr(ctx).Or(ArithExpr(ctx));
}
static Parser<ExpressionPart> ExprPart(Context ctx)
{
return Expr(ctx).Select(body =>
{
Expression stringBody = body.Type == typeof(string)
? body
: Expression.Call(body, typeof(object).GetMethod("ToString", Type.EmptyTypes)!);
return new ExpressionPart { Expr = Expression.Lambda<Func<Context, string>>(stringBody, CtxParam) };
});
}
static readonly Parser<string> XPath =
from path in Parse.LetterOrDigit
.XOr(Parse.Char('/'))
.XOr(Parse.Char(' '))
.XOr(Parse.Char('['))
.XOr(Parse.Char(']'))
.XOr(Parse.Char('='))
.XOr(Parse.Char('.'))
.XOr(Parse.Char('_'))
.XOr(Parse.Char('*'))
.XOr(Parse.Char('@'))
.XOr(Parse.Char('#'))
.XOr(Parse.Char('('))
.XOr(Parse.Char(')'))
.XOr(Parse.Char('\''))
.Many()
select new string(path.ToArray());
static Parser<T> Selector<T>(Parser<T> selection)
{
return from open in Parse.String("$(\"").Token()
from s in selection
from close in Parse.String("\")").Token()
select s;
}
public static readonly Parser<string> XPathSelector = Selector(XPath);
public static readonly Parser<string> Identifier =
from first in Parse.Letter.Once()
from rest in Parse.LetterOrDigit.XOr(Parse.Char('-')).XOr(Parse.Char('_')).Many()
select new string(first.Concat(rest).ToArray());
private static readonly Parser<string> StringObject =
from open in Parse.Char('"')
from value in Parse.CharExcept('"').Many().Text()
from close in Parse.Char('"')
select new string(value);
private static readonly Parser<string> NumberObject =
Parse.DecimalInvariant
.Select(s => double.Parse(s, CultureInfo.InvariantCulture))
.Select(v => v.ToString());
public static readonly Parser<string> Argument =
from argument in StringObject.Or(NumberObject)
select argument;
public static readonly Parser<Call> CallObject =
from identifier in Identifier
from open in Parse.Char('(')
from arguments in Argument.DelimitedBy(Parse.Char(',').Token())
from close in Parse.Char(')')
select new Call{Identifier = new string(identifier.ToArray()), Arguments = arguments};
/*public static readonly Parser<Expression> ExpressionObject =
from expression in CallObject
*/
public static CommentParser Comment = new CommentParser { Single = "#", NewLine = Environment.NewLine };
public static readonly Parser<Part> BangObject =
from bang in Parse.Char('!')
from name in Parse.AnyChar.Until(Parse.Char('('))
from arguments in Argument.DelimitedBy(Parse.Char(',').Token())
from bclose in Parse.Char(')')
let ctx = new Context{XPathSelector = "//*."}
from ws1 in Parse.WhiteSpace.Many().Optional()
from open in Parse.Char('{')
from ws2 in Parse.WhiteSpace.Many().Optional()
from expressions in Parse.Ref(() => BangObject)
.Or(Parse.Ref(() => RuleObject))
.Or(ExprPart(ctx).Select(e => (Part)e))
.DelimitedBy(Parse.Char(';').Token())
from tailingsemi in Parse.Char(';').Optional()
from ws3 in Parse.WhiteSpace.Many().Optional()
from close in Parse.Char('}')
select new Bang{Name = new string([.. name]), Arguments = arguments, Body = expressions};
public static readonly Parser<Part> RuleObject =
from selector in Selector(XPath).Token()
let ctx = new Context{XPathSelector = selector}
from ws1 in Parse.WhiteSpace.Many().Optional()
from open in Parse.Char('{')
from ws2 in Parse.WhiteSpace.Many().Optional()
from expressions in Parse.Ref(() => BangObject)
.Or(Parse.Ref(() => RuleObject))
.Or(ExprPart(ctx).Select(e => (Part)e))
.DelimitedBy(Parse.Char(';').Token())
from tailingsemi in Parse.Char(';').Optional()
from ws3 in Parse.WhiteSpace.Many().Optional()
from close in Parse.Char('}')
select new Rule{XPathSelector = selector, Body = expressions};
static public string StripComments(string source)
{
var result = new StringBuilder();
foreach (var line in source.Split('\n'))
{
bool inSingle = false, inDouble = false;
int commentIdx = -1;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '\'' && !inDouble) inSingle = !inSingle;
else if (c == '"' && !inSingle) inDouble = !inDouble;
else if (c == '#' && !inSingle && !inDouble)
{
commentIdx = i;
break;
}
}
result.Append(commentIdx >= 0 ? line[..commentIdx] : line);
result.Append('\n');
}
return result.ToString();
}
}
}