Merge MVP features #1

Merged
dominic merged 16 commits from prototype into master 2026-04-24 18:48:16 +00:00
7 changed files with 85 additions and 14 deletions
Showing only changes of commit 1899db4156 - Show all commits

29
Common/SourceLocation.cs Normal file
View File

@ -0,0 +1,29 @@
using System.Text;
namespace XNeedle
{
/// <summary>
/// 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).
/// </summary>
public struct SourceLocation
{
public string FilePath { get; set; }
public Encoding Encoding { get; set; }
/// <summary>
/// 1-based line number of the associated node, or -1 if unknown.
/// </summary>
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);
}
}

View File

@ -57,10 +57,33 @@ namespace XNeedle.Formats
return new XmlRoundtripWriter(xmlWriter);
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Detects encoding from BOM first, then the XML declaration, then defaults to UTF-8.
/// </summary>
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);

View File

@ -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()
{

View File

@ -13,15 +13,13 @@ namespace XNeedle.Parser
from rbraket in Parse.Char('>')
select new string([.. any]);
}
public static Parser<string> ChecksumData(string opening, string closing)
public static Parser<string> 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;
}
}
}

View File

@ -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}\"");

View File

@ -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)

View File

@ -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<Part> 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);