using System; using System.Collections.Generic; using System.Linq; using System.Text; using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Sprache; using System.IO; using System.Globalization; using System.Reflection.Metadata; using System.Xml; using System.Threading.Tasks.Dataflow; public class Context { public required string XPathSelector { get; set; } public XmlNodeList? Nodes { get; set; } public XmlNode? Node { get; set; } } public class ContextFunctions { public static string hex(Context ctx, double value) { uint v = (uint)value; return $"#x{v:X4}"; } public static int GetLevel(XmlNode node) { int level = 0; while (null != (node = node.ParentNode)) level++; return level; } public static double crc(Context ctx) { int crc = 0xAFFE; XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.ConformanceLevel = ConformanceLevel.Fragment; // This isn't working, need to render the _entire_ xml each time :( using (var sw = new StringWriter()) { using (var xw = XmlWriter.Create(sw, settings)) { ctx.Node.WriteContentTo(xw); } var data = sw.ToString(); var indentLevel = GetLevel(ctx.Node); var extraIndent = new string(' ', indentLevel); data = extraIndent + data.ReplaceLineEndings(Environment.NewLine + extraIndent); var crc32 = new System.IO.Hashing.Crc32(); var bytes = Encoding.UTF8.GetBytes(data); crc32.Append(bytes); crc = BitConverter.ToInt32(crc32.GetCurrentHash()); } return (double)crc; } public static string text(Context ctx) { Console.WriteLine($"Get T: {ctx.Node.InnerText}"); return ctx.Node.InnerText; } public static string text(Context ctx, string txt) { Console.WriteLine($"Set T: {txt}"); return ctx.Node.InnerText = txt; } public static string attr(Context ctx, string name, string value) { if (ctx.Node.Attributes[name] != null) { ctx.Node.Attributes[name].Value = value; } else { var add = ctx.Node.OwnerDocument.CreateAttribute(name); add.Value = value; ctx.Node.Attributes.SetNamedItem(add); } return ""; } public static string attr(Context ctx, string name) { if (ctx.Node.Attributes[name] != null) { return ctx.Node.Attributes[name].Value; } return ""; } } public class Part { public virtual void Operate(Context ctx) {} } public class ExpressionPart : Part { public required Expression> Expr { get; set; } public override void Operate(Context xtx) { Expr.Compile()(); } } public class Evaluatable : Part { public virtual string Evaluate(Context ctx) { return ""; } } public class Call : Evaluatable { public required string Identifier { get; set; } public required IEnumerable Arguments; } public class Arithmeric : Evaluatable { public required Expression> Expr { get; set; } public override string Evaluate(Context ctx) { return Expr.Compile()().ToString(); } public override void Operate(Context xtx) { Expr.Compile()(); } } public class Rule : Part { public required string XPathSelector { get; set; } public required IEnumerable Body; public required Context Context; public void Transform(XmlDocument xml) { Context.Node = xml.DocumentElement; Operate(Context); } public override void Operate(Context ctx) { Context.Nodes = ctx.Node.SelectNodes(Context.XPathSelector); foreach (XmlNode n in Context.Nodes) { Context.Node = n; foreach (var b in Body) { b.Operate(Context); } } } } public static class TrafoParser { static Parser 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 Add = Operator("+", ExpressionType.AddChecked); static readonly Parser Subtract = Operator("-", ExpressionType.SubtractChecked); static readonly Parser Multiply = Operator("*", ExpressionType.MultiplyChecked); static readonly Parser Divide = Operator("/", ExpressionType.Divide); static readonly Parser Modulo = Operator("%", ExpressionType.Modulo); static readonly Parser Power = Operator("^", ExpressionType.Power); /* static readonly Parser Function = from name in Parse.Letter.AtLeastOnce().Text() from lparen in Parse.Char('(') from expr in Parse.Ref(() => Expr).DelimitedBy(Parse.Char(',').Token()) from rparen in Parse.Char(')') select CallFunction(name, expr.ToArray()); */ static Parser 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(Expression.Constant(ctx)).ToArray()); } throw new ParseException(string.Format("Function '{0}({1})' does not exist.", name, string.Join(",", parameters.Select(e => e.Type.Name)))); } static readonly Parser NumberConstant = Parse.Decimal .Select(x => Expression.Constant(double.Parse(x))) .Named("number"); /* static readonly Parser Factor = (from lparen in Parse.Char('(') from expr in Parse.Ref(() => Expr) from rparen in Parse.Char(')') select expr).Named("expression") .XOr(Constant) .XOr(Function); */ private static readonly Parser 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 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 readonly Parser Operand = ((from sign in Parse.Char('-') from factor in Factor select Expression.Negate(factor) ).XOr(Factor)).Token(); */ static Parser Operand(Context ctx) { return ((from sign in Parse.Char('-') from factor in Factor(ctx) select Expression.Negate(factor) ).XOr(Factor(ctx))).Token(); } //static readonly Parser InnerTerm = Parse.ChainRightOperator(Power, Operand, Expression.MakeBinary); static Parser InnerTerm(Context ctx) { return Parse.ChainRightOperator(Power, Operand(ctx), Expression.MakeBinary); } //static readonly Parser Term = Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo), InnerTerm, Expression.MakeBinary); static Parser Term(Context ctx) { return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo), InnerTerm(ctx), Expression.MakeBinary); } //static readonly Parser Expr = Parse.ChainOperator(Add.Or(Subtract), Term, Expression.MakeBinary); static Parser 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 ArithExpr(Context ctx) { return Parse.ChainOperator(Add.Or(Subtract), Term(ctx), Expression.MakeBinary); } static Parser Expr(Context ctx) { return ConcatExpr(ctx).Or(ArithExpr(ctx)); } /*static readonly Parser>> Lambda = Expr.End().Select(body => Expression.Lambda>(body)); */ static Parser ExprPart(Context ctx) { return Expr(ctx).Select(body => new ExpressionPart{Expr = Expression.Lambda>(body)}); } static readonly Parser 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 Selector(Parser selection) { return from open in Parse.String("$(\"").Token() from s in selection from close in Parse.String("\")").Token() select s; } public static readonly Parser XPathSelector = Selector(XPath); public static readonly Parser 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 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 NumberObject = Parse.DecimalInvariant .Select(s => double.Parse(s, CultureInfo.InvariantCulture)) .Select(v => v.ToString()); public static readonly Parser Argument = from argument in StringObject.Or(NumberObject) select argument; public static readonly Parser 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 ExpressionObject = from expression in CallObject */ public static CommentParser Comment = new CommentParser { Single = "#", NewLine = Environment.NewLine }; public static readonly Parser RuleObject = from c1 in Comment.SingleLineComment.Optional() 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 c2 in Comment.SingleLineComment.Optional() //from expressions in RuleObject.Select(n => (Rule)n).DelimitedBy(';') //from expressions in ExprPart(new Context{XPathSelector = selector}).Or(RuleObject).Select(n => (Part)n).DelimitedBy(Parse.Char(';').Token()) from expressions in ExprPart(ctx).Or(RuleObject).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, Context = ctx}; } class Program { static void Main() { var input = "$(\"//Device/(Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000'])[1]/..\")"; input = @" $(""/EtherCATInfo/Vendor/Name"") { $(""Element"") { attr(""foo"", ""bar""); Sin(2); } text(""Fake Beckhoff""); } "; input = @" $(""/EtherCATInfo/Vendor/Name"") { text(""Foobar "" .. text()); $(""Element"") { attr(""foo"", ""bar""); }; } "; input = @" $(""//EtherCATInfo/Vendor/Name"") { text(""Fake "" .. text()); attr(""Crc"", hex(3735928559)); } "; input = @" $(""//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']/.."") { $(""Name[@LcId='1033']"") { text(""EL2904, 4 Ch. Danger Output 24V, 0.5A, TwinUNSAFE""); }; $(""RxPdo/Index[.='#x1600']/.."") { $(""Name"") { text(""FSoE Outputs""); }; $(""Entry/Index[.='#x7000']"") { text(""#x4711""); }; attr(""fid"", ""1""); attr(""FaultId"", attr(""fid"")); attr(""Crc32x"", hex(crc()+1)); } } "; var parsed = TrafoParser.RuleObject.Parse(input); XmlDocument doc = new XmlDocument(); doc.Load("ELx9xx.xml"); parsed.Transform(doc); XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; XmlWriter writer = XmlWriter.Create("El9xxFi.xml", settings); doc.WriteContentTo(writer); Console.WriteLine("Done."); } }