XPathCompiler
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
namespace Castle.Components.DictionaryAdapter.Xml
{
public static class XPathCompiler
{
private enum Token
{
Name,
StepSeparator,
NameSeparator,
AttributeStart,
VariableStart,
EqualsOperator,
PredicateStart,
PredicateEnd,
StringLiteral,
EndOfInput,
Error
}
private class Tokenizer
{
private enum State
{
Initial,
Name,
SingleQuoteString,
DoubleQuoteString,
Failed
}
private readonly string input;
private State state;
private Token token;
private int index;
private int start;
private int prior;
public Token Token => token;
public string Text => input.Substring(start, index - start + 1);
public int Index => start;
public Tokenizer(string input)
{
this.input = input;
state = State.Initial;
index = -1;
Consume();
}
public string GetConsumedText(int start)
{
return input.Substring(start, prior - start + 1);
}
public void Consume()
{
prior = index;
while (true) {
char c = ReadChar();
switch (state) {
case State.Initial:
start = index;
switch (c) {
case '/':
token = Token.StepSeparator;
return;
case ':':
token = Token.NameSeparator;
return;
case '@':
token = Token.AttributeStart;
return;
case '$':
token = Token.VariableStart;
return;
case '=':
token = Token.EqualsOperator;
return;
case '[':
token = Token.PredicateStart;
return;
case ']':
token = Token.PredicateEnd;
return;
case ' ':
token = Token.EndOfInput;
return;
case '\'':
state = State.SingleQuoteString;
break;
case '"':
state = State.DoubleQuoteString;
break;
default:
if (IsNameStartChar(c))
state = State.Name;
else if (!IsWhitespace(c)) {
state = State.Failed;
}
break;
}
break;
case State.Name:
if (!IsNameChar(c)) {
RewindChar();
token = Token.Name;
state = State.Initial;
return;
}
break;
case State.SingleQuoteString:
if (c == '\'') {
token = Token.StringLiteral;
state = State.Initial;
return;
}
break;
case State.DoubleQuoteString:
if (c == '"') {
token = Token.StringLiteral;
state = State.Initial;
return;
}
break;
case State.Failed:
token = Token.Error;
return;
}
}
}
private char ReadChar()
{
if (++index >= input.Length)
return ' ';
return input[index];
}
private void RewindChar()
{
index--;
}
private static bool IsWhitespace(char c)
{
return XmlConvert.IsWhitespaceChar(c);
}
private static bool IsNameStartChar(char c)
{
return XmlConvert.IsStartNCNameChar(c);
}
private static bool IsNameChar(char c)
{
return XmlConvert.IsNCNameChar(c);
}
}
public static CompiledXPath Compile(string path)
{
if (path == null)
throw Error.ArgumentNull("path");
CompiledXPath compiledXPath = new CompiledXPath();
compiledXPath.Path = XPathExpression.Compile(path);
Tokenizer source = new Tokenizer(path);
if (!ParsePath(source, compiledXPath))
compiledXPath.MakeNotCreatable();
compiledXPath.Prepare();
return compiledXPath;
}
private static bool ParsePath(Tokenizer source, CompiledXPath path)
{
if (!ParseStep(source, out CompiledXPathStep step))
return false;
path.FirstStep = step;
CompiledXPathStep compiledXPathStep = step;
path.Depth = 1;
while (true) {
if (source.Token == Token.EndOfInput)
return true;
if (source.Token != Token.StepSeparator)
return false;
if (compiledXPathStep.IsAttribute)
return false;
source.Consume();
if (!ParseStep(source, out step))
break;
compiledXPathStep.NextStep = step;
step.PreviousNode = compiledXPathStep;
compiledXPathStep = step;
path.Depth++;
}
return false;
}
private static bool ParseStep(Tokenizer source, out CompiledXPathStep step)
{
step = new CompiledXPathStep();
int index = source.Index;
if (!ParseNodeCore(source, step))
return false;
string consumedText = source.GetConsumedText(index);
step.Path = XPathExpression.Compile(consumedText);
return true;
}
private static bool ParseNodeCore(Tokenizer source, CompiledXPathNode node)
{
if (Consume(source, Token.AttributeStart))
node.IsAttribute = true;
if (!ParseQualifiedName(source, node))
return false;
if (!ParsePredicateList(source, node))
return false;
return true;
}
private static bool ParsePredicateList(Tokenizer source, CompiledXPathNode node)
{
IList<CompiledXPathNode> dependencies = node.Dependencies;
while (source.Token == Token.PredicateStart) {
if (!ParsePredicate(source, dependencies))
return false;
}
return true;
}
private static bool ParsePredicate(Tokenizer source, IList<CompiledXPathNode> dependencies)
{
source.Consume();
if (!ParseAndExpression(source, dependencies))
return false;
if (!Consume(source, Token.PredicateEnd))
return false;
return true;
}
private static bool ParseAndExpression(Tokenizer source, IList<CompiledXPathNode> dependencies)
{
while (true) {
if (!ParseExpression(source, out CompiledXPathNode node))
return false;
dependencies.Add(node);
if (source.Token != 0 || source.Text != "and")
break;
source.Consume();
}
return true;
}
private static bool ParseExpression(Tokenizer source, out CompiledXPathNode node)
{
if (source.Token != 0 && source.Token != Token.AttributeStart)
return ParseRightToLeftExpression(source, out node);
return ParseLeftToRightExpression(source, out node);
}
private static bool ParseLeftToRightExpression(Tokenizer source, out CompiledXPathNode node)
{
if (!ParsePathExpression(source, out node))
return false;
if (!Consume(source, Token.EqualsOperator))
return true;
if (!ParseValue(source, out XPathExpression value))
return false;
GetLastNode(node).Value = value;
return true;
}
private static bool ParseRightToLeftExpression(Tokenizer source, out CompiledXPathNode node)
{
if (!ParseValue(source, out XPathExpression value))
return Try.Failure(out node);
if (!Consume(source, Token.EqualsOperator))
return Try.Failure(out node);
if (!ParsePathExpression(source, out node))
return false;
GetLastNode(node).Value = value;
return true;
}
private static bool ParsePathExpression(Tokenizer source, out CompiledXPathNode node)
{
if (!ParseNode(source, out node))
return false;
CompiledXPathNode compiledXPathNode = node;
while (true) {
if (!Consume(source, Token.StepSeparator))
return true;
if (compiledXPathNode.IsAttribute)
return false;
if (!ParseNode(source, out CompiledXPathNode node2))
break;
compiledXPathNode.NextNode = node2;
node2.PreviousNode = compiledXPathNode;
compiledXPathNode = node2;
}
return false;
}
private static bool ParseNode(Tokenizer source, out CompiledXPathNode node)
{
node = new CompiledXPathNode();
return ParseNodeCore(source, node);
}
private static bool ParseValue(Tokenizer source, out XPathExpression value)
{
int index = source.Index;
if (!Consume(source, Token.StringLiteral) && (!Consume(source, Token.VariableStart) || !ParseQualifiedName(source, null)))
return Try.Failure(out value);
string consumedText = source.GetConsumedText(index);
value = XPathExpression.Compile(consumedText);
return true;
}
private static bool ParseQualifiedName(Tokenizer source, CompiledXPathNode node)
{
if (!ParseName(source, out string name))
return false;
if (!Consume(source, Token.NameSeparator)) {
if (node != null)
node.LocalName = name;
return true;
}
if (!ParseName(source, out string name2))
return false;
if (node != null) {
node.Prefix = name;
node.LocalName = name2;
}
return true;
}
private static bool ParseName(Tokenizer source, out string name)
{
if (source.Token != 0)
return Try.Failure(out name);
name = source.Text;
source.Consume();
return true;
}
private static bool Consume(Tokenizer source, Token token)
{
if (source.Token != token)
return false;
source.Consume();
return true;
}
private static CompiledXPathNode GetLastNode(CompiledXPathNode node)
{
while (true) {
CompiledXPathNode nextNode = node.NextNode;
if (nextNode == null)
break;
node = nextNode;
}
return node;
}
}
}