From f4a563a9d4d9e845fc0e921e89f95bd0ce28d12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Fri, 24 Apr 2026 19:16:07 +0200 Subject: [PATCH] Add "!add" bang --- Common/SourceLocation.cs | 2 ++ Program.cs | 3 +++ Transformation/Bang.cs | 31 ++++++++++++++++++++++++++++++ Transformation/BangOperation.cs | 31 ++++++++++++++++++++++++++++++ Transformation/Bangs/AddBang.cs | 28 +++++++++++++++++++++++++++ Transformation/ContextFunctions.cs | 27 +++++++++++++++++++++++++- 6 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 Transformation/Bang.cs create mode 100644 Transformation/BangOperation.cs create mode 100644 Transformation/Bangs/AddBang.cs diff --git a/Common/SourceLocation.cs b/Common/SourceLocation.cs index 31fafd0..4422cfe 100644 --- a/Common/SourceLocation.cs +++ b/Common/SourceLocation.cs @@ -25,5 +25,7 @@ namespace XNeedle public SourceLocation WithLineNumber(int lineNumber) => new SourceLocation(FilePath, Encoding, lineNumber); + public SourceLocation AsSynthetic() => + new SourceLocation("", Encoding, -1); } } diff --git a/Program.cs b/Program.cs index 0e37562..cd3f6a0 100644 --- a/Program.cs +++ b/Program.cs @@ -35,6 +35,9 @@ namespace XNeedle static void RunOptions(Options opts) { + // Auto-discover operations via reflection + BangOperation.RegisterAll(); + var masterRule = (Rule)XnParser.RuleObject.Parse(File.ReadAllText(opts.XnDefinition)); var doc = XDocument.Load(opts.Input, LoadOptions.PreserveWhitespace); var encoding = XmlRoundtripWriter.DetectEncoding(File.ReadAllBytes(opts.Input)); diff --git a/Transformation/Bang.cs b/Transformation/Bang.cs new file mode 100644 index 0000000..f52b627 --- /dev/null +++ b/Transformation/Bang.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using XNeedle; +using XNeedle.Parser.Elements; + +namespace XNeedle.Transformation +{ + public class Bang : Part + { + public required string Name { get; set; } + public required IEnumerable Arguments; + public required IEnumerable Body; + public override void Operate(Context ctx) + { + var bangop = BangOperation.Resolve(Name); + if (bangop == null) + { + throw new ArgumentException($"!{Name} is not a valid bang."); + } + + var bodyctx = bangop.Execute(ctx, Arguments); + Console.WriteLine($"Operate Bang: {Name}, {string.Join(",", Arguments)}"); + foreach (var b in Body) + { + b.Operate(bodyctx); + } + } + } +} \ No newline at end of file diff --git a/Transformation/BangOperation.cs b/Transformation/BangOperation.cs new file mode 100644 index 0000000..385d3e8 --- /dev/null +++ b/Transformation/BangOperation.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using XNeedle.Parser.Elements; + +namespace XNeedle.Transformation +{ + public abstract class BangOperation + { + private static readonly Dictionary Registry = new(); + + public abstract string Name { get; } + + protected BangOperation() + { + Registry[Name] = this; + } + + public abstract Context Execute(Context ctx, IEnumerable arguments); + + public static BangOperation? Resolve(string name) => + Registry.TryGetValue(name, out var op) ? op : null; + + public static void RegisterAll() + { + foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) + if (!type.IsAbstract && type.IsSubclassOf(typeof(BangOperation))) + Activator.CreateInstance(type); + } + } +} \ No newline at end of file diff --git a/Transformation/Bangs/AddBang.cs b/Transformation/Bangs/AddBang.cs new file mode 100644 index 0000000..d68683d --- /dev/null +++ b/Transformation/Bangs/AddBang.cs @@ -0,0 +1,28 @@ +using System.Xml.Linq; +using XNeedle.Parser.Elements; + +namespace XNeedle.Transformation.Bangs +{ + public class AddBang : BangOperation + { + public override string Name => "add"; + + public override Context Execute(Context ctx, IEnumerable arguments) + { + if (ctx.Node == null) + { + throw new ArgumentNullException("!add needs a reachable node"); + } + if (arguments.Count() < 1) + { + throw new ArgumentException("!add needs at least the tag name as argument."); + } + + var tagname = arguments.ElementAt(0); + var element = new XElement(tagname, ""); + ctx.Node.Add(element); + + return new Context{ Node = element, SLoc = ctx.SLoc.AsSynthetic(), XPathSelector = tagname}; + } + } +} \ No newline at end of file diff --git a/Transformation/ContextFunctions.cs b/Transformation/ContextFunctions.cs index ed29e44..acefd4a 100644 --- a/Transformation/ContextFunctions.cs +++ b/Transformation/ContextFunctions.cs @@ -5,6 +5,8 @@ using Sprache; using XNeedle.Formats; using XNeedle.Parser; using XNeedle.Parser.Elements; +using System.Xml.Linq; +using System.Xml.XPath; namespace XNeedle.Transformation { @@ -19,7 +21,7 @@ namespace XNeedle.Transformation public static double GetLevel(Context ctx) { int level = 0; - System.Xml.Linq.XElement? n = ctx.Node; + XElement? n = ctx.Node; while (null != (n = n?.Parent)) level++; return level; @@ -106,5 +108,28 @@ namespace XNeedle.Transformation } return ""; } + + public static string swap(Context ctx, string a_select, string b_select) + { + if (ctx.Node == null) + { + throw new ArgumentNullException("!seap 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 ""; + } } } \ No newline at end of file