Compare commits

..

16 Commits

Author SHA1 Message Date
3bd86319a6 Update README.md 2026-04-24 20:39:10 +02:00
326cf57870 Final touches 2026-04-24 20:38:53 +02:00
08e5dd8ef5 Add error reporting 2026-04-24 20:20:50 +02:00
6f94002cf4 Fix comment handling
Clanker: claude-sonnet_v4.6
2026-04-24 20:11:08 +02:00
9a3f91e41c Add more CRUD. 2026-04-24 19:50:39 +02:00
f4a563a9d4 Add "!add" bang 2026-04-24 19:16:07 +02:00
c93864c7e4 Add Bang parser
One minor bug found after claude review.

Clanker: claude-sonnet_4.6
2026-04-24 06:21:59 +02:00
fa6d9e3ace Add bang parsing 2026-04-24 06:03:03 +02:00
1899db4156 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
2026-04-24 05:28:39 +02:00
c694fe8b17 Add cdata manipulators
Clanker: claude-sonnet_4.6
2026-04-24 04:45:42 +02:00
154647083c fix: port transformation layer from XmlDocument to XDocument
- Thread runtime Context through expression trees via ParameterExpression
  instead of baking a parse-time constant (Node was always null)
- Fix ExpressionPart to compile Func<Context,string> and invoke with live ctx
- Coerce double arithmetic expressions to string before lambda wrapping
- Remove HasAttributes guard in attr() that blocked new attribute creation
- Replace GetLevel(XmlNode) with XElement.Parent-based implementation
- Add cdata()/cdata(txt) functions to read/write CDATA nodes in place,
  preserving the XCData wrapper that SetValue() would destroy

Clanker: claude-sonnet_4.6
2026-04-24 04:44:59 +02:00
2d54bec41d Clanker made roundtripping possible 2026-04-24 04:33:37 +02:00
cc8738c0dc Use LINQ XDocument 2026-04-23 20:20:56 +02:00
13e375c145 Add command line interface 2026-04-23 19:55:24 +02:00
6ef074081a Restructured project, removed comment-out code 2026-04-23 19:11:53 +02:00
c71b85c8fe Add initial prototype 2025-11-20 20:03:12 +01:00
28 changed files with 59 additions and 595 deletions

View File

@ -1,14 +0,0 @@
# Contributors
## Original Author
**Dominic Hoegliner**
Author and maintainer of XN.
All intellectual property rights retained by the original author.
---
Contributions via pull request are welcome.
By submitting a contribution, you agree that your contribution
will be licensed under the same BSD 3-Clause License as this project,
and that the original authorship of Dominic Hoegliner is acknowledged.

View File

@ -1,4 +1,4 @@
Copyright 2026 Dominic Hoeglinger
Copyright 2025 DOMINIC HOEGLINER
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View File

@ -12,7 +12,6 @@ using Rule = XNeedle.Transformation.Rule;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Security.AccessControl;
namespace XNeedle.Parser
{
@ -31,15 +30,12 @@ namespace XNeedle.Parser
static readonly Parser<ExpressionType> Multiply = Operator("*", ExpressionType.MultiplyChecked);
static readonly Parser<ExpressionType> Divide = Operator("/", ExpressionType.Divide);
static readonly Parser<ExpressionType> Modulo = Operator("%", ExpressionType.Modulo);
static readonly Parser<ExpressionType> Power = Operator("**", ExpressionType.Power);
static readonly Parser<ExpressionType> BitwiseXor = Operator("^", ExpressionType.ExclusiveOr);
static readonly Parser<ExpressionType> BitwiseAnd = Operator("&", ExpressionType.And);
static readonly Parser<ExpressionType> BitwiseOr = Operator("|", ExpressionType.Or);
static readonly Parser<ExpressionType> Power = Operator("^", ExpressionType.Power);
static Parser<Expression> Function(Context ctx)
{
return
from name in Identifier
from name in Parse.Letter.AtLeastOnce().Text()
from lparen in Parse.Char('(')
from expr in Parse.Ref(() => Expr(ctx)).DelimitedBy(Parse.Char(',').Token()).Optional()
from rparen in Parse.Char(')')
@ -50,51 +46,23 @@ namespace XNeedle.Parser
{
var mathMethodInfo = typeof(Math).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).ToArray());
if (mathMethodInfo != null)
{
return Expression.Call(mathMethodInfo, parameters);
// Find by name and fill in optional parameters with their default values
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo()
.GetMethods()
.FirstOrDefault(m =>
{
if (m.Name != name) return false;
var mp = m.GetParameters();
if (mp.Length == 0 || mp[0].ParameterType != typeof(Context)) return false;
int required = mp.Skip(1).Count(p => !p.HasDefaultValue);
return parameters.Length >= required && parameters.Length <= mp.Length - 1;
});
}
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).Prepend(typeof(Context)).ToArray());
if (ctxMethodInfo != null)
{
var mp = ctxMethodInfo.GetParameters();
var args = mp.Skip(1).Select((p, i) =>
i < parameters.Length
? parameters[i]
: Expression.Constant(p.DefaultValue, p.ParameterType)
).ToArray();
return Expression.Call(ctxMethodInfo, args.Prepend(CtxParam).ToArray());
return Expression.Call(ctxMethodInfo, parameters.Prepend(CtxParam).ToArray());
}
throw new ParseException(string.Format("Function '{0}({1})' does not exist.", name,
string.Join(",", parameters.Select(e => e.Type.Name))));
}
// This needs to match as one atomic token, else with a separate String("0x") the 0 is consumed,
// which triggers a parsing error on single zero numbers
public static readonly Parser<string> HexNumber =
Parse.Regex(@"0x[0-9a-fA-F]+").Token()
.Select(s => s.Substring(2));
public static readonly Parser<string> BooleanLiteral =
from value in Parse.String("true").XOr(Parse.String("false")).Text()
select value;
static readonly Parser<Expression> NumberConstant =
HexNumber.Select(x => Expression.Constant((double)int.Parse(x, System.Globalization.NumberStyles.HexNumber)))
.XOr(BooleanLiteral.Select(x => Expression.Constant(x == "true" ? 1.0 : 0.0)))
.XOr(Parse.Decimal.Select(x => Expression.Constant(double.Parse(x))))
.Named("number");
Parse.Decimal
.Select(x => Expression.Constant(double.Parse(x)))
.Named("number");
private static readonly Parser<Expression> StringConstant =
from open in Parse.Char('"')
@ -127,18 +95,7 @@ namespace XNeedle.Parser
static Parser<Expression> Term(Context ctx)
{
return Parse.ChainOperator(Multiply.Or(Divide), InnerTerm(ctx), Expression.MakeBinary);
}
static Parser<Expression> IntegerTerm(Context ctx)
{
// Integer operations can't deal with doubles, so we need to make unary casts to integer and back
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo).Or(BitwiseXor).Or(BitwiseAnd).Or(BitwiseOr),
InnerTerm(ctx), (bt,left,right)=>Expression.MakeUnary(
ExpressionType.Convert, Expression.MakeBinary(bt,
Expression.MakeUnary(ExpressionType.Convert, left, typeof(long)),
Expression.MakeUnary(ExpressionType.Convert, right, typeof(long))),
typeof(double)));
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo), InnerTerm(ctx), Expression.MakeBinary);
}
static Parser<Expression> ConcatExpr(Context ctx)
@ -153,7 +110,7 @@ namespace XNeedle.Parser
static Parser<Expression> ArithExpr(Context ctx)
{
return Parse.ChainOperator(Add.Or(Subtract), IntegerTerm(ctx).Or(Term(ctx)), Expression.MakeBinary);
return Parse.ChainOperator(Add.Or(Subtract), Term(ctx), Expression.MakeBinary);
}
static Parser<Expression> Expr(Context ctx)
@ -226,6 +183,10 @@ namespace XNeedle.Parser
from arguments in Argument.DelimitedBy(Parse.Char(',').Token())
from close in Parse.Char(')')
select new Call{Identifier = new string(identifier.ToArray()), Arguments = arguments};
/*public static readonly Parser<Expression> ExpressionObject =
from expression in CallObject
*/
public static CommentParser Comment = new CommentParser { Single = "#", NewLine = Environment.NewLine };
@ -238,13 +199,11 @@ namespace XNeedle.Parser
from ws1 in Parse.WhiteSpace.Many().Optional()
from open in Parse.Char('{')
from ws2 in Parse.WhiteSpace.Many().Optional()
from expressions in (
from expr in Parse.Ref(() => BangObject)
.Or(Parse.Ref(() => RuleObject))
.Or(ExprPart(ctx).Select(e => (Part)e))
from semi in Parse.Char(';').Token()
select expr
).Many()
from expressions in Parse.Ref(() => BangObject)
.Or(Parse.Ref(() => RuleObject))
.Or(ExprPart(ctx).Select(e => (Part)e))
.DelimitedBy(Parse.Char(';').Token())
from tailingsemi in Parse.Char(';').Optional()
from ws3 in Parse.WhiteSpace.Many().Optional()
from close in Parse.Char('}')
select new Bang{Name = new string([.. name]), Arguments = arguments, Body = expressions};
@ -255,23 +214,15 @@ namespace XNeedle.Parser
from ws1 in Parse.WhiteSpace.Many().Optional()
from open in Parse.Char('{')
from ws2 in Parse.WhiteSpace.Many().Optional()
from expressions in (
from expr in Parse.Ref(() => BangObject)
.Or(Parse.Ref(() => RuleObject))
.Or(ExprPart(ctx).Select(e => (Part)e))
from semi in Parse.Char(';').Token()
select expr
).Many()
from expressions in Parse.Ref(() => BangObject)
.Or(Parse.Ref(() => RuleObject))
.Or(ExprPart(ctx).Select(e => (Part)e))
.DelimitedBy(Parse.Char(';').Token())
from tailingsemi in Parse.Char(';').Optional()
from ws3 in Parse.WhiteSpace.Many().Optional()
from close in Parse.Char('}')
select new Rule{XPathSelector = selector, Body = expressions};
public static readonly Parser<IEnumerable<Part>> Rules =
(from rule in RuleObject.Token()
from semi in Parse.Char(';').Token()
select rule)
.Many()
.End();
static public string StripComments(string source)
{

View File

@ -40,10 +40,10 @@ namespace XNeedle
var rawSource = File.ReadAllText(opts.XnDefinition);
var xnSource = XnParser.StripComments(rawSource);
IEnumerable<Part> rules;
Rule masterRule;
try
{
rules = XnParser.Rules.Parse(xnSource);
masterRule = (Rule)XnParser.RuleObject.Parse(xnSource);
}
catch (ParseException ex)
{
@ -54,19 +54,11 @@ namespace XNeedle
if (showPosition)
{
var msg = ex.Message;
var hint = (msg.Contains("expected }") && !msg.Contains("expected ';'"))
? "Hint: a missing ';' terminator may be located just after this position"
: "";
Console.Error.WriteLine($"{opts.XnDefinition}({line},{col}): parse error: {msg}");
Console.Error.WriteLine($"{opts.XnDefinition}({line},{col}): parse error: {ex.Message}");
var srcLine = line <= lines.Length ? lines[line - 1].TrimEnd() : "";
var pointer = new string(' ', Math.Max(0, col - 1)) + "^";
Console.Error.WriteLine($" {srcLine}");
Console.Error.WriteLine($" {pointer}");
if (!string.IsNullOrEmpty(hint))
{
Console.Error.WriteLine($"\n{hint}");
}
}
else
{
@ -77,16 +69,7 @@ namespace XNeedle
var doc = XDocument.Load(opts.Input, LoadOptions.PreserveWhitespace);
var encoding = XmlRoundtripWriter.DetectEncoding(File.ReadAllBytes(opts.Input));
var sloc = new SourceLocation(opts.Input, encoding);
try
{
foreach (var rule in rules.OfType<Rule>())
rule.Transform(doc, sloc);
}
catch (RequireException ex)
{
Console.Error.WriteLine($"{ex.SLoc.FilePath}({ex.SLoc.LineNumber}): require error: {ex.UserMessage}");
return;
}
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,15 +1,14 @@
# XN
# XNeedle
XML Needle (XN) is a simple tool for injecting faults into XML documents.
A simple tool for injecting faults into XML documents.
## Description
The purpose of the tool is to hide a metaphorical "needle in a haystack"
in large XML files, such as an EtherCAT SubDevice Information or ESI file.
Therefore it is a FIT (fault injection test) tool,
but can be used for general purpose manipulation.
Therefore it is a FIT (fault injection test) tool, but can be used off-label.
XPath is utilized to specify precise locations
To live up to its name, XPath is utilized to specify precise locations
combined with a good enough set of instructions to do CRUD operations.
As the main application lies with injecting faults into ESI files,
@ -19,7 +18,7 @@ such as modules in an ESI file.
## Language Specification
The XN language consists of three distinct parts.
The XNeedle language consists of three distinct parts.
1. Expressions
@ -29,7 +28,7 @@ and values.
This example prepends the existing inner text of a given node with "Faulty '",
and appends the evaluated string "': 2+2=10".
```
text("Faulty '" .. text() .. "': " .. string(1+1**2) .. "+2=10");
text("Faulty '" .. text() .. "': " .. string(1+1^2) .. "+2=10");
```
2. Rules
@ -51,7 +50,7 @@ $("//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']/..")
3. Bangs
Bangs are special function calls which take either a string or XPath as parameters,
and make addition changes to the document.
and make structural changes to the document.
They are prefixed with, as the name implies, an exclamation mark.
This example adds, removes and swaps nodes.
@ -59,16 +58,16 @@ This example adds, removes and swaps nodes.
$("RxPdo/Index[.="#x1600"]/..")
{
# Modify name
$("Name") { text("FSoE Outputs"); }
$(Name) { text("FSoE Outputs"); }
# Select the Entry/Index #x7000 and change it to #x9001
$("Entry/Index[.='#x7000']") { text("#x9001"); }
$(Entry/Index[.="#x7000"]) { text("#x9001"); }
# Select the Entry with Index #x7001 and SubIndex 4, remove it
$("Entry/Index[.='#x7001']/../SubIndex[.="4"]/..") { remove(); }
$(Entry/Index[.="#x7001"]/../SubIndex[.="4"]/..) { remove(); }
# Select the 3rd Entry
$("Entry[3]")
$(Entry[3])
{
# Add a tag before the above selection
!before("Entry")

View File

@ -1,5 +1,4 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using Sprache;
@ -13,62 +12,10 @@ namespace XNeedle.Transformation
{
public class ContextFunctions
{
public static double num(Context ctx, string value, double numbase = 10)
{
if (string.IsNullOrWhiteSpace(value))
{
return 0;
}
string cleanedValue = value.Trim();
if (numbase != 10)
{
cleanedValue = cleanedValue.TrimStart('0');
cleanedValue = cleanedValue.TrimStart('x');
// Convert to Int64 to not lose any fidelity
return Convert.ToInt64(cleanedValue, (int)numbase);
}
return double.Parse(cleanedValue, CultureInfo.InvariantCulture);
}
public static string str(Context ctx, double value, double numbase = 10)
{
if (numbase == 10)
{
return $"{value}";
}
// Ignore fraction for other bases by casting to long
return Convert.ToString((long)value, (int)numbase);
}
public static string hexlit(Context ctx, double value)
public static string hex(Context ctx, double value)
{
uint v = (uint)value;
return $"#x{v:x8}";
}
public static double parselit(Context ctx, string literal, double numbase = 16)
{
if (string.IsNullOrWhiteSpace(literal) || literal.Length <= 2)
{
throw new ArgumentException($"Cannot parse literal \"{literal}\"");
}
return (double)Convert.ToInt32(literal.Substring(2), (int)numbase);
}
public static string lower(Context ctx, string text)
{
return text.ToLower();
}
public static string upper(Context ctx, string text)
{
return text.ToUpper();
return $"#x{v:X8}";
}
public static double GetLevel(Context ctx)
@ -79,41 +26,7 @@ namespace XNeedle.Transformation
level++;
return level;
}
private static uint ComputeCrc32(byte[] data, uint poly, uint init, uint xorOut, bool reflected)
{
uint crc = init;
uint polyReflected = ReflectBits(poly, 32);
foreach (byte b in data)
{
if (reflected)
{
crc ^= b;
for (int i = 0; i < 8; i++)
crc = (crc & 1) != 0 ? (crc >> 1) ^ polyReflected : crc >> 1;
}
else
{
crc ^= (uint)b << 24;
for (int i = 0; i < 8; i++)
crc = (crc & 0x80000000u) != 0 ? (crc << 1) ^ poly : crc << 1;
}
}
return crc ^ xorOut;
}
private static uint ReflectBits(uint value, int bits)
{
uint reflected = 0;
for (int i = 0; i < bits; i++)
{
if ((value & (1u << i)) != 0)
reflected |= 1u << (bits - 1 - i);
}
return reflected;
}
public static double crc32(Context ctx, double poly = 0x04C11DB7, double init = 0, double xorOut = 0, double reflected = 0.0)
public static double crc(Context ctx)
{
if (ctx.Node is null)
return 0;
@ -123,15 +36,17 @@ namespace XNeedle.Transformation
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);
//byte[] fragBytes = ctx.SLoc.Encoding.GetBytes(data);
byte[] fragBytes = Encoding.UTF8.GetBytes(data);
// Step 3: Digest into CRC32 using parameterized CRC32 - by default MPEG-2
return (double)ComputeCrc32(fragBytes, (uint)poly, (uint)init, (uint)xorOut, reflected == 1.0);
// 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)
@ -272,18 +187,5 @@ namespace XNeedle.Transformation
}
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 "";
}
}
}

View File

@ -22,19 +22,15 @@ namespace XNeedle.Transformation
public override void Operate(Context ctx)
{
var nodes = ctx.Node?.XPathSelectElements(XPathSelector);
if(nodes is null || nodes.Count() == 0)
if(ctx.Node is null)
{
return;
}
var nodes = ctx.Node.XPathSelectElements(XPathSelector);
if(nodes is null)
{
var nullCtx = ctx.Clone();
nullCtx.Node = null;
nullCtx.SLoc = ctx.SLoc.WithLineNumber(-1);
foreach (var b in Body)
{
b.Operate(nullCtx);
}
return;
}
foreach (XElement n in nodes)
{
var loopCtx = ctx.Clone();

View File

@ -1,76 +0,0 @@
# XN Syntax
## Comments
Comments are single line only prefixed with the pound sign ('#').
## Rules
Rules consists of two parts, a selector, and a block containing statements.
A selector is an XPath string surrounded with braces prefixed with a dollar sign.
The XPath selects zero or more document nodes which will serve as the context for the statement block.
## Bangs
Bangs are function calls combined with a statement block.
They are prefixed with an exclaimation mark.
A bang function always produces context nodes for the statement block.
## Statement Block
A statement block are multiple statements terminated with a semicolon,
surrounded by curly braces.
## Statements
A statement can be an expression, a function call, a rule or a bang.
To note is that a nested rule will use its parents context to refine the selection further.
## Expressions
An expression can consist of function calls, operators, strings or numbers.
Operators supported:
| Operator | Function |
|----------|----------------------|
| + | addition |
| - | subtraction |
| * | multiplication |
| / | division |
| ** | power |
| & | bitwise and |
| | | bitwise or |
| ^ | bitwise exclusive or |
| .. | string concatenate |
## Example
```
# Select the RxPdo with Index 0x1600
$("RxPdo/Index[.="#x1600"]/..")
{
# Function call
swap("Entry[3]", "Entry[4]");
# Of that RxPdo, select the 3rd Entry via a nested rule
$("Entry[3]")
{
# This bang has "RxPdo/Index[.="#x1600"]/../Entry[3]" as its context
!before("Entry")
{
# Function call contains an expression, which evaluates here to "!#xFFFF"
!add("Index") { text("!"..hexlit(2**16-1)); };
!add("SubIndex") { text("3"); }
!add("BitLen") { text("256"); }
!add("Name") { text("Test"); }
!add("DataType") { text("UINT"); }
}
}
}
# One .xn can contain multiple rules
$("//Module[@Crc32]")
{
attr("Crc32", hexlit(crc32()));
};
```

View File

@ -1,204 +0,0 @@
# Function Reference
## `num(value:string, numbase:number = 10):number`
Converts a string to a number in respect of the input base.
Example:
```xn
# Add 1 to all LcId
$("//Name[@LcId]")
{
attr("LcId", str(num(attr("LcId")) + 1));
};
```
## `str(value:number, numbase:number = 10):string`
Converts a number to a string in any base.
Example:
```xn
# Set ProfileNo to "#o377"
$("//ProfileNo")
{
text("#o"..str(2**8-1, 8));
};
```
## `hexlit(value:number):string`
Produces a hex literal from an integer with lowercase digits.
Example:
```xn
# Set all module Crc32 to #x00c0ffee
$("//Module[@Crc32]")
{
attr("Crc32", hexlit(0xC0FFEE));
};
```
## `parselit(literal:string, base:number = 16):string`
Parses a literal number to an integer.
Example:
```xn
# Increase all RevisionNo by 1
$("//Device/Type[@RevisionNo]")
{
attr("RevisionNo", hexlit(parselit(attr("RevisionNo")) + 1));
};
```
## `lower(text:string):string`
Converts a string to all lower case text.
Example:
```xn
# Convert all device names to lower case
$("//Device/Type")
{
$("Name") { cdata(lower(cdata())); };
}
```
## `upper(text:string):string`
Converts a string to all upper case text.
Example:
```xn
# Convert all Name tag text in Index nodes from RxPdo to upper case.
$("//RxPdo/Index/Name")
{
$("Name") { text(upper(text())); };
}
```
## `crc32(poly:number = 0x04C11DB7, init:number = 0, xorOut:number = 0, reflected:bool = false):number`
Calculates the CRC32 checksum from the rendered document starting after the tag close of the context
node up to and including the closing tag.
Example:
For this example `[IND]` is whitespace for indent,
`[CR]` for carriage return and `[LF]` for line feed.
Rendered document:
```xml
<root>[CR][LF]
[IND]<node attribute="yes">[CR][LF]
[IND][IND]<child/>
[IND][IND]Text
[IND]</node>
</root>
XN
```xn
# Calculate CRC of node and store in "crc" attribute as uppercase hexlit
$("//Node") { attr("crc", upper(hexlit(crc32())); }
```
Digested fragment:
```txt
[CR][LF]
[IND][IND]<child/>
[IND][IND]Text
[IND]</node>
```
## `text():string` / `text(value:string):string`
Get or set the text of the context node.
Example:
```xn
# Prefix all Name tag text in Index nodes from TxPdo with "Fault".
$("//TxPdo/Index/Name")
{
$("Name") { text("Fault" .. text()); };
}
```
## `cdata():string` / `cdata(value:string):string`
Get or set the CDATA of the context node.
Example:
```xn
# For Device with this ProductCode and Revision, modify its CDATA text
$("//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']/..")
{
$("Name[@LcId='1033']") { cdata("LE2904, 4 Ch. Danger Output 24V, 0.5A, TwinUNSAFE"); };
}
```
## `attr(name:string):string` / `attr(name:string, value:string):string`
Get or set an attribute of the context node by name.
Example:
```xn
# For Device with this ProductCode and Revision, set the revision to #xc0deaffe
$("//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']")
{
attr("RevisionNo", hexlit(0xC0DEAFFE));
}
```
## `swap(xpath_a:string, xpath_b:string):string = ""`
Swaps two nodes by XPath in the current cotext node.
Example:
```xn
# Swap first and second Entry in RxPdo of Index 0x1600
$("//RxPdo/Index[.='#x1600']/..") { swap("Entry[1]", "Entry[2]"); };
```
## `remove():string = ""`
Removes the context node from the document.
Example:
```xn
# Remove all modules
$("//Module") { remove(); };
```
## `rename(name:string):string = ""`
Renames the context node.
Example:
```xn
# Rename all Device nodes to DeviceX
$("//Device") { rename("DeviceX"); };
```
## `rename(xpath:string, name:string):string = ""`
Renames any nodes matching the XPath in the context node.
Example:
```xn
# Rename all Device nodes Name tags to Label
$("//Device") { rename("Name", "Label"); };
```
## `require(message:string):string = ""`
If the context node is not present, emit an error message.
Example:
```xn
# Emit an error if no Module tags are in the document
$("//Module") { require("No Module definitions!"); };
```

View File

@ -1,53 +0,0 @@
# Bang Reference
## `!add(name:string)`
Appends a new node into the context.
Example:
```xn
# Add 1 to all LcId
$("//RxPdo/Index[.="#x1600"]/..")
{
# Add a new Entry with attribute "added" set to "true"
!add("Entry")
{
attr("added", "true");
}
};
```
## `!before(name:string)`
Appends a new node before the context.
Example:
```xn
# Add 1 to all LcId
$("//RxPdo/Index[.="#x1600"]/..")
{
# Add a new TxPdo with attribute "added" set to "true" before the above node selection
!before("TxPdo")
{
attr("added", "true");
}
};
```
## `!after(name:string)`
Appends a new node after the context.
Example:
```xn
# Add 1 to all LcId
$("//RxPdo/Index[.="#x1600"]/..")
{
# Add a new TxPdo with attribute "added" set to "true" after the above node selection
!after("TxPdo")
{
attr("added", "true");
}
};
```

View File

@ -1,15 +0,0 @@
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 RequireException : Exception
{
public SourceLocation SLoc { get; set; }
public required string UserMessage { get; set; }
}
}

View File

@ -1,5 +0,0 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/xn.csproj" />
</Folder>
</Solution>