Files
Umbraco-CMS/src/Umbraco.Core/Xml/UmbracoXPathPathSyntaxParser.cs

123 lines
5.2 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
2021-09-14 22:13:39 +02:00
using System.Globalization;
using System.Linq;
2022-02-28 13:14:02 +01:00
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Xml
{
/// <summary>
2017-07-20 11:21:28 +02:00
/// This is used to parse our customize Umbraco XPath expressions (i.e. that include special tokens like $site) into
/// a real XPath statement
/// </summary>
public class UmbracoXPathPathSyntaxParser
{
/// <summary>
/// Parses custom umbraco xpath expression
/// </summary>
/// <param name="xpathExpression">The Xpath expression</param>
/// <param name="nodeContextId">
/// The current node id context of executing the query - null if there is no current node, in which case
/// some of the parameters like $current, $parent, $site will be disabled
/// </param>
/// <param name="getPath">The callback to create the nodeId path, given a node Id</param>
/// <param name="publishedContentExists">The callback to return whether a published node exists based on Id</param>
/// <returns></returns>
public static string ParseXPathQuery(
2017-07-20 11:21:28 +02:00
string xpathExpression,
int? nodeContextId,
2022-02-28 13:14:02 +01:00
Func<int, IEnumerable<string>?> getPath,
Func<int, bool> publishedContentExists)
{
// TODO: This should probably support some of the old syntax and token replacements, currently
// it does not, there is a ticket raised here about it: http://issues.umbraco.org/issue/U4-6364
2019-01-22 18:03:39 -05:00
// previous tokens were: "$currentPage", "$ancestorOrSelf", "$parentPage" and I believe they were
2017-07-20 11:21:28 +02:00
// allowed 'inline', not just at the beginning... whether or not we want to support that is up
// for discussion.
if (xpathExpression == null) throw new ArgumentNullException(nameof(xpathExpression));
if (string.IsNullOrWhiteSpace(xpathExpression)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(xpathExpression));
2017-05-31 09:18:09 +02:00
if (getPath == null) throw new ArgumentNullException(nameof(getPath));
if (publishedContentExists == null) throw new ArgumentNullException(nameof(publishedContentExists));
//no need to parse it
if (xpathExpression.StartsWith("$") == false)
return xpathExpression;
//get nearest published item
2022-02-28 13:14:02 +01:00
Func<IEnumerable<string>?, int> getClosestPublishedAncestor = path =>
{
2022-02-28 13:14:02 +01:00
if (path is not null)
{
2022-02-28 13:14:02 +01:00
foreach (var i in path)
{
2022-02-28 13:14:02 +01:00
int idAsInt;
if (int.TryParse(i, NumberStyles.Integer, CultureInfo.InvariantCulture, out idAsInt))
{
var exists = publishedContentExists(int.Parse(i, CultureInfo.InvariantCulture));
if (exists)
return idAsInt;
}
}
}
2022-02-28 13:14:02 +01:00
return -1;
2022-02-28 13:14:02 +01:00
};
2017-09-18 15:33:13 +02:00
const string rootXpath = "id({0})";
//parseable items:
var vars = new Dictionary<string, Func<string, string>>();
//These parameters must have a node id context
if (nodeContextId.HasValue)
{
vars.Add("$current", q =>
{
var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value));
return q.Replace("$current", string.Format(rootXpath, closestPublishedAncestorId));
});
vars.Add("$parent", q =>
{
//remove the first item in the array if its the current node
//this happens when current is published, but we are looking for its parent specifically
2022-02-28 13:14:02 +01:00
var path = getPath(nodeContextId.Value)?.ToArray();
if (path?[0] == nodeContextId.ToString())
{
2022-02-28 13:14:02 +01:00
path = path?.Skip(1).ToArray();
}
var closestPublishedAncestorId = getClosestPublishedAncestor(path);
return q.Replace("$parent", string.Format(rootXpath, closestPublishedAncestorId));
});
vars.Add("$site", q =>
{
var closestPublishedAncestorId = getClosestPublishedAncestor(getPath(nodeContextId.Value));
return q.Replace("$site", string.Format(rootXpath, closestPublishedAncestorId) + "/ancestor-or-self::*[@level = 1]");
});
}
// TODO: This used to just replace $root with string.Empty BUT, that would never work
// the root is always "/root . Need to confirm with Per why this was string.Empty before!
vars.Add("$root", q => q.Replace("$root", "/root"));
foreach (var varible in vars)
{
if (xpathExpression.StartsWith(varible.Key))
{
xpathExpression = varible.Value(xpathExpression);
break;
}
}
return xpathExpression;
}
}
}