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 = "" + tagName + ">";
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("" + closing + ">")
- 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);