xn/Transformation/ContextFunctions.cs
Dominic Höglinger a05903df4d Add stricter semicolon grammar, require() function, fix multi-rule xn
To easily spot semicolon errors, they are now
mandatory delimiters after each construct.

Add require() to raise an error if a match is unsucessful.

Multiple rules in one xn files now possible.
2026-04-25 01:19:25 +02:00

204 lines
6.4 KiB
C#

using System;
using System.Linq;
using System.Text;
using Sprache;
using XNeedle.Formats;
using XNeedle.Parser;
using XNeedle.Parser.Elements;
using System.Xml.Linq;
using System.Xml.XPath;
namespace XNeedle.Transformation
{
public class ContextFunctions
{
public static string hex(Context ctx, double value)
{
uint v = (uint)value;
return $"#x{v:X8}";
}
public static double GetLevel(Context ctx)
{
int level = 0;
XElement? n = ctx.Node;
while (null != (n = n?.Parent))
level++;
return level;
}
public static double crc(Context ctx)
{
if (ctx.Node is null)
return 0;
// Step 1: Render the element to a string, preserving source style
var (writer, buffer) = XmlRoundtripWriter.CreateToString(ctx.SLoc.FilePath, ctx.SLoc.Encoding);
ctx.Node.WriteTo(writer);
writer.Flush();
string xml = buffer.ToString();
// Step 2: Extract all text after the opening tag through and including the closing tag
string tagName = ctx.Node.Name.LocalName;
string data = XmlFragParser.ChecksumData(tagName).Parse(xml);
// Step 3: Digest into CRC32 using the source encoding
var crc32 = new System.IO.Hashing.Crc32();
crc32.Append(ctx.SLoc.Encoding.GetBytes(data));
uint result = BitConverter.ToUInt32(crc32.GetCurrentHash());
return (double)result;
}
public static string text(Context ctx)
{
if (ctx.Node is null) return "";
return ctx.Node.Value;
}
public static string text(Context ctx, string txt)
{
if (ctx.Node is null) return "";
ctx.Node.SetValue(txt);
return txt;
}
public static string cdata(Context ctx)
{
if (ctx.Node is null) return "";
var cdataNode = ctx.Node.Nodes().OfType<System.Xml.Linq.XCData>().FirstOrDefault();
return cdataNode?.Value ?? "";
}
public static string cdata(Context ctx, string txt)
{
if (ctx.Node is null) return "";
var cdataNode = ctx.Node.Nodes().OfType<System.Xml.Linq.XCData>().FirstOrDefault();
if (cdataNode != null)
{
cdataNode.Value = txt;
}
else
{
ctx.Node.Add(new System.Xml.Linq.XCData(txt));
}
return txt;
}
public static string attr(Context ctx, string name, string value)
{
if (ctx.Node is null) return "";
var attr = ctx.Node.Attribute(name);
if (attr != null)
{
attr.Value = value;
}
else
{
ctx.Node.SetAttributeValue(name, value);
}
return "";
}
public static string attr(Context ctx, string name)
{
var attr = ctx.Node?.Attribute(name);
if (attr != null)
{
return attr.Value;
}
return "";
}
public static string swap(Context ctx, string a_select, string b_select)
{
if (ctx.Node == null)
{
throw new ArgumentNullException("!swap needs a reachable node");
}
var a_node = ctx.Node.XPathSelectElement(a_select);
var b_node = ctx.Node.XPathSelectElement(b_select);
if (a_node == null || b_node == null)
{
throw new InvalidOperationException($"!swap could not find nodes for \"{a_select}\" and/or \"{b_select}\".");
}
var placeholder = new XElement("__placeholder__");
a_node.ReplaceWith(placeholder);
b_node.ReplaceWith(a_node);
placeholder.ReplaceWith(b_node);
return "";
}
public static string remove(Context ctx, string select)
{
if (ctx.Node == null)
{
throw new ArgumentNullException("!remove needs a reachable node");
}
// Use ToList to make the lazy/generator enumeration concrete before applying operations
var nodes = ctx.Node.XPathSelectElements(select).ToList();
foreach (var node in nodes)
{
node.Remove();
}
return "";
}
public static string rename(Context ctx, string newName)
{
if (ctx.Node is null) return "";
var replacement = new XElement(newName, ctx.Node.Attributes(), ctx.Node.Nodes());
ctx.Node.ReplaceWith(replacement);
ctx.Node = replacement; // update context to point to new element
return "";
}
public static string rename(Context ctx, string select, string newName)
{
if (ctx.Node is null) return "";
int atIdx = select.IndexOf('@');
if (atIdx >= 0)
{
// "Node@Attr" — rename an attribute
var elementSelect = select[..atIdx].TrimEnd();
var attrName = select[(atIdx + 1)..].TrimStart();
var target = string.IsNullOrEmpty(elementSelect)
? ctx.Node
: ctx.Node.XPathSelectElement(elementSelect);
var attr = target?.Attribute(attrName);
if (attr is null) return "";
attr.Remove();
target!.SetAttributeValue(newName, attr.Value);
}
else
{
// plain XPath select — rename element
var target = ctx.Node.XPathSelectElement(select);
if (target is null) return "";
var replacement = new XElement(newName, target.Attributes(), target.Nodes());
target.ReplaceWith(replacement);
}
return "";
}
public static string require(Context ctx, string message = "Required node not present")
{
if (ctx.Node == null)
{
throw new RequireException{SLoc=ctx.SLoc, UserMessage=message};
}
else
{
Console.WriteLine(ctx.Node.ToString());
}
return "";
}
}
}