From 1899db41568e1aa1a9be530dea76b8b594a1c556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Fri, 24 Apr 2026 05:28:39 +0200 Subject: [PATCH] feat: implement ETG.2000-compatible CRC32 for XNeedle transforms - Add SourceLocation struct (XNeedle namespace) with FilePath, Encoding, and LineNumber (-1 sentinel when unavailable) - Add SLoc property to Context; Clone() propagates it for free - Expose XmlRoundtripWriter.DetectEncoding as internal static - Add XmlRoundtripWriter.CreateToString(filePath, encoding) returning a (writer, StringBuilder) tuple for in-memory style-preserving rendering - Update XmlFragParser.ChecksumData to accept tagName only; use OpeningTagAnchor to skip the opening tag robustly, and AnyChar.Until() to non-greedily capture content through the closing tag - Thread SourceLocation from Program.cs through Rule.Transform/Operate, refreshing LineNumber per node via IXmlLineInfo - Implement ContextFunctions.crc: render node via CreateToString, extract digest slice via ChecksumData, encode with SLoc.Encoding, return CRC32 as double for arithmetic compatibility Clanker: claude-sonnet_4.6 --- Common/SourceLocation.cs | 29 +++++++++++++++++++++++++++++ Formats/XmlRoundtripWriter.cs | 25 ++++++++++++++++++++++++- Parser/Elements/Context.cs | 1 + Parser/XmlFragParser.cs | 12 +++++------- Program.cs | 4 +++- Transformation/ContextFunctions.cs | 22 ++++++++++++++++++---- Transformation/Rule.cs | 6 +++++- 7 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 Common/SourceLocation.cs diff --git a/Common/SourceLocation.cs b/Common/SourceLocation.cs new file mode 100644 index 0000000..31fafd0 --- /dev/null +++ b/Common/SourceLocation.cs @@ -0,0 +1,29 @@ +using System.Text; + +namespace XNeedle +{ + /// + /// Carries the origin information for a loaded XML document: + /// the file path, the detected encoding, and the line number of the + /// associated node (-1 when line information is unavailable). + /// + public struct SourceLocation + { + public string FilePath { get; set; } + public Encoding Encoding { get; set; } + /// + /// 1-based line number of the associated node, or -1 if unknown. + /// + public int LineNumber { get; set; } + + public SourceLocation(string filePath, Encoding encoding, int lineNumber = -1) + { + FilePath = filePath; + Encoding = encoding; + LineNumber = lineNumber; + } + + public SourceLocation WithLineNumber(int lineNumber) => + new SourceLocation(FilePath, Encoding, lineNumber); + } +} diff --git a/Formats/XmlRoundtripWriter.cs b/Formats/XmlRoundtripWriter.cs index 6dcec1f..cf5918b 100644 --- a/Formats/XmlRoundtripWriter.cs +++ b/Formats/XmlRoundtripWriter.cs @@ -57,10 +57,33 @@ namespace XNeedle.Formats return new XmlRoundtripWriter(xmlWriter); } + /// + /// Creates an XmlRoundtripWriter that writes to a StringBuilder for in-memory rendering. + /// Style detection (compact self-closing tags, DOS line endings) is read from the source file. + /// + public static (XmlRoundtripWriter writer, StringBuilder buffer) CreateToString(string filePath, Encoding encoding) + { + var rawBytes = File.ReadAllBytes(filePath); + var rawText = encoding.GetString(rawBytes); + + var compact = Regex.IsMatch(rawText, @"\S/>"); + var dosLineEndings = rawText.Contains("\r\n"); + + var sb = new StringBuilder(); + var stringWriter = new StringWriter(sb); + var filterWriter = new FilteringTextWriter(stringWriter, compact, dosLineEndings, verbatimEncodingName: null); + var xmlWriter = new XmlTextWriter(filterWriter) + { + Formatting = Formatting.None + }; + + return (new XmlRoundtripWriter(xmlWriter), sb); + } + /// /// Detects encoding from BOM first, then the XML declaration, then defaults to UTF-8. /// - private static Encoding DetectEncoding(byte[] raw) + internal static Encoding DetectEncoding(byte[] raw) { if (raw.Length >= 3 && raw[0] == 0xEF && raw[1] == 0xBB && raw[2] == 0xBF) return new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); diff --git a/Parser/Elements/Context.cs b/Parser/Elements/Context.cs index 53d9057..9f15c50 100644 --- a/Parser/Elements/Context.cs +++ b/Parser/Elements/Context.cs @@ -6,6 +6,7 @@ namespace XNeedle.Parser.Elements { public string? XPathSelector { get; set; } public XElement? Node { get; set; } + public SourceLocation SLoc { get; set; } public Context Clone() { diff --git a/Parser/XmlFragParser.cs b/Parser/XmlFragParser.cs index 88acf58..b41a716 100644 --- a/Parser/XmlFragParser.cs +++ b/Parser/XmlFragParser.cs @@ -13,15 +13,13 @@ namespace XNeedle.Parser from rbraket in Parse.Char('>') select new string([.. any]); } - public static Parser ChecksumData(string opening, string closing) + public static Parser ChecksumData(string tagName) { + var closing = ""; return - from pregarbage in Parse.AnyChar.Many() - from preamble in Parse.String(opening).Token() - from content in Parse.AnyChar.Many() - from epilouge in Parse.String("") - from postgarbage in Parse.AnyChar.Many() - select new string([.. content]) + epilouge; + from preamble in OpeningTagAnchor(tagName) + from content in Parse.AnyChar.Until(Parse.String(closing)) + select new string([.. content]) + closing; } } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 6703d9f..0e37562 100644 --- a/Program.cs +++ b/Program.cs @@ -37,7 +37,9 @@ namespace XNeedle { var masterRule = (Rule)XnParser.RuleObject.Parse(File.ReadAllText(opts.XnDefinition)); var doc = XDocument.Load(opts.Input, LoadOptions.PreserveWhitespace); - masterRule.Transform(doc); + var encoding = XmlRoundtripWriter.DetectEncoding(File.ReadAllBytes(opts.Input)); + var sloc = new SourceLocation(opts.Input, encoding); + masterRule.Transform(doc, sloc); using var writer = XmlRoundtripWriter.Create(opts.Input, opts.Output); doc.WriteTo(writer); Console.WriteLine($"Applied \"{opts.XnDefinition}\" to \"{opts.Input}\" and saved result to \"{opts.Output}\""); diff --git a/Transformation/ContextFunctions.cs b/Transformation/ContextFunctions.cs index b4ef284..ed29e44 100644 --- a/Transformation/ContextFunctions.cs +++ b/Transformation/ContextFunctions.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Text; using Sprache; +using XNeedle.Formats; using XNeedle.Parser; using XNeedle.Parser.Elements; @@ -25,12 +27,24 @@ namespace XNeedle.Transformation public static double crc(Context ctx) { if (ctx.Node is null) - { return 0; - } - int crc = 0xAFFE; - return (double)crc; + // 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) diff --git a/Transformation/Rule.cs b/Transformation/Rule.cs index 961844c..a51e897 100644 --- a/Transformation/Rule.cs +++ b/Transformation/Rule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; +using XNeedle; using XNeedle.Parser.Elements; namespace XNeedle.Transformation @@ -11,10 +12,11 @@ namespace XNeedle.Transformation public required string XPathSelector { get; set; } public required IEnumerable Body; - public void Transform(XDocument xml) + public void Transform(XDocument xml, SourceLocation sloc) { var context = new Context(); context.Node = xml.Root; + context.SLoc = sloc; Operate(context); } @@ -33,6 +35,8 @@ namespace XNeedle.Transformation { var loopCtx = ctx.Clone(); loopCtx.Node = n; + var lineInfo = (IXmlLineInfo)n; + loopCtx.SLoc = ctx.SLoc.WithLineNumber(lineInfo.HasLineInfo() ? lineInfo.LineNumber : -1); foreach (var b in Body) { b.Operate(loopCtx);