diff --git a/src/HtmlAgilityPack.Shared/HtmlAgilityPack.Shared.projitems b/src/HtmlAgilityPack.Shared/HtmlAgilityPack.Shared.projitems index fa59426f..f2af311d 100644 --- a/src/HtmlAgilityPack.Shared/HtmlAgilityPack.Shared.projitems +++ b/src/HtmlAgilityPack.Shared/HtmlAgilityPack.Shared.projitems @@ -57,5 +57,8 @@ + + + \ No newline at end of file diff --git a/src/HtmlAgilityPack.Shared/XPathContext.cs b/src/HtmlAgilityPack.Shared/XPathContext.cs new file mode 100644 index 00000000..5152552b --- /dev/null +++ b/src/HtmlAgilityPack.Shared/XPathContext.cs @@ -0,0 +1,219 @@ +/* First released in Dawnx library, it is now available to HAP under the MIT License */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.XPath; +using System.Xml.Xsl; + +namespace HtmlAgilityPack +{ + public abstract partial class XPathContext : XsltContext + { + private class ContextFunction + { + public string Namespace { get; set; } + public string Name { get; set; } + public Type[] ArgTypes { get; set; } + public Type[] RealArgTypes { get; set; } + public MethodInfo Method { get; set; } + } + + private HashSet CustomFunctions = new HashSet(); + + public XPathContext(string prefix) : this() + { + AddNamespace(prefix, DefaultNamespace); + } + public XPathContext() + { + var contextMethods = GetType().GetMethods(); + foreach (var method in contextMethods) + { + var attr = method + .GetCustomAttributes(typeof(XPathFunctionAttribute), true) + .FirstOrDefault() as XPathFunctionAttribute; + if (attr != null) + { + CustomFunctions.Add(new ContextFunction + { + Namespace = attr.Namespace ?? DefaultNamespace, + Name = attr.Name ?? method.Name, + ArgTypes = method.GetParameters() + .Where(x => x.ParameterType != typeof(XPathNavigator)) + .Select(x => x.ParameterType) + .ToArray(), + RealArgTypes = method.GetParameters().Select(x => x.ParameterType).ToArray(), + Method = method, + }); + } + } + } + + /// + /// Gets all the defined argumennts in the context. + /// + public XsltArgumentList ArgList { get; private set; } = new XsltArgumentList(); + + /// + /// Evaluates whether to preserve white space nodes or strip them for the given context. + /// + /// + /// + public override bool PreserveWhitespace(XPathNavigator node) => false; + + /// + /// Compares the base Uniform Resource Identifiers + /// (URIs) of two documents based upon the order the documents were loaded by the + /// XSLT processor (that is, the System.Xml.Xsl.XslTransform class) + /// + /// + /// + /// + public override int CompareDocument(string baseUri, string nextbaseUri) => 0; + + /// + /// Gets a value indicating whether to include white space nodes in the output. + /// + public override bool Whitespace => true; + + /// + /// Resolves a function reference and returns + /// an representing the function. The + /// is used at execution time to get the return value of the function. + /// + /// + /// + /// + /// + public override IXsltContextFunction ResolveFunction(string prefix, string name, XPathResultType[] argTypes) + => new XPathFunctionAgent(LookupNamespace(prefix), name); + + /// + /// Resolves a variable reference and returns + /// an System.Xml.Xsl.IXsltContextVariable representing the variable. + /// + /// + /// + /// + public override IXsltContextVariable ResolveVariable(string prefix, string name) + => new XPathVariable(prefix, name); + + /// + /// Adds a argument to and associates it with the namespace qualified name. + /// + /// + /// + /// + public void AddParam(string name, string namespaceUri, object parameter) + => ArgList.AddParam(name, namespaceUri, parameter); + + /// + /// Adds a argument to and associates it with empty namespace. + /// + /// + /// + public void AddParam(string name, object parameter) + => ArgList.AddParam(name, "", parameter); + + /// + /// Compiles the XPath expression specified and returns an System.Xml.XPath.XPathExpression + /// object representing the XPath expression. + /// + /// + /// + public XPathExpression Compile(string xpath) + { + var xExp = XPathExpression.Compile(xpath); + xExp.SetContext(this); + return xExp; + } + public XPathExpression this[string xpath] => Compile(xpath); + + public class XPathFunctionAgent : IXsltContextFunction + { + private string Namespace; + private string Name; + + public XPathFunctionAgent(string @namespace, string name) + { + Namespace = @namespace; + Name = name; + } + + public int Minargs => throw new NotSupportedException(); + public int Maxargs => throw new NotSupportedException(); + public XPathResultType ReturnType => XPathResultType.Any; + public XPathResultType[] ArgTypes => throw new NotSupportedException(); + + public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext) + { + var context = xsltContext as XPathContext; + var argTypes = args.Select(x => + { + switch (x.GetType().FullName) + { + case "MS.Internal.Xml.XPath.XPathSelectionIterator": return typeof(string); + default: return x.GetType(); + } + }); + var customFunc = context.CustomFunctions + .FirstOrDefault(x => x.Namespace == Namespace && x.Name == Name + && Enumerable.SequenceEqual(argTypes, x.ArgTypes)); + + if (customFunc != null) + { + var methodParameterLength = customFunc.Method.GetParameters().Count(); + var funcArgs = args.Select((arg, i) => + { + switch (arg.GetType().FullName) + { + case "MS.Internal.Xml.XPath.XPathSelectionIterator": return GetAttributeValue(args[i]); + default: return args[i].ToString(); + } + }).ToArray(); + + int argIndex = 0; + var invokeParameters = new List(); + foreach (var realArgType in customFunc.RealArgTypes) + { + switch (realArgType) + { + case Type _ when realArgType == typeof(XPathNavigator): + invokeParameters.Add(docContext); + break; + + default: + invokeParameters.Add(funcArgs[argIndex++]); + break; + } + } + return customFunc.Method.Invoke(context, invokeParameters.ToArray()); + } + else throw new KeyNotFoundException($"No function found. ({Namespace}.{Name})"); + } + + private string GetAttributeValue(object arg) + { + // The type of arg is MS.Internal.Xml.XPath.XPathSelectionIterator. + var currentProp = arg.GetType().GetProperty("Current"); + var current = currentProp.GetValue(arg, null) as XPathNavigator; + + switch (current.NodeType) + { + case XPathNodeType.Element: + foreach (XPathNavigator item in arg as IEnumerable) + return item.InnerXml; + goto default; + + default: + return string.Empty; + } + } + + } + + } +} diff --git a/src/HtmlAgilityPack.Shared/XPathFunctionAttribute.cs b/src/HtmlAgilityPack.Shared/XPathFunctionAttribute.cs new file mode 100644 index 00000000..0a59d3e8 --- /dev/null +++ b/src/HtmlAgilityPack.Shared/XPathFunctionAttribute.cs @@ -0,0 +1,36 @@ +/* First released in Dawnx library, it is now available to HAP under the MIT License */ + +using System; +using System.Xml.XPath; + +namespace HtmlAgilityPack +{ + [AttributeUsage(AttributeTargets.Method)] + public class XPathFunctionAttribute : Attribute + { + public string Namespace { get; private set; } + public string Name { get; private set; } + + /// + /// Defines a function named '{DefaultNamespace}:{$name}' in the context. + /// Project each into the function's arguments. + /// (If you need the 'docContext', you must use a parameter to receive it.) + /// + /// + public XPathFunctionAttribute(string name) : this(null, name) { } + + /// + /// Defines a function named '{$namespace}:{$name}' in the context. + /// Project each into the function's arguments. + /// (If you need the 'docContext', you must use a parameter to receive it.) + /// + /// + /// + public XPathFunctionAttribute(string namespaceUri, string name) + { + Namespace = namespaceUri; + Name = name; + } + + } +} \ No newline at end of file diff --git a/src/HtmlAgilityPack.Shared/XPathVariable.cs b/src/HtmlAgilityPack.Shared/XPathVariable.cs new file mode 100644 index 00000000..dbd61729 --- /dev/null +++ b/src/HtmlAgilityPack.Shared/XPathVariable.cs @@ -0,0 +1,30 @@ +/* First released in Dawnx library, it is now available to HAP under the MIT License */ + +using System.Xml.XPath; +using System.Xml.Xsl; + +namespace HtmlAgilityPack +{ + internal class XPathVariable : IXsltContextVariable + { + public string Prefix { get; private set; } + public string Name { get; private set; } + + public XPathVariable(string prefix, string name) + { + Prefix = prefix; + Name = name; + } + + public object Evaluate(XsltContext xsltContext) + { + var argList = ((XPathContext)xsltContext).ArgList; + return argList.GetParam(Name, xsltContext.LookupNamespace(Prefix)); + } + + public bool IsLocal => false; + public bool IsParam => true; + public XPathResultType VariableType => XPathResultType.Any; + + } +}