2015-05-12 12:39:46 +10:00
using System ;
using System.Collections.Generic ;
2021-09-14 22:13:39 +02:00
using System.Globalization ;
2015-05-12 12:39:46 +10:00
using System.Linq ;
2021-02-18 11:06:02 +01:00
namespace Umbraco.Cms.Core.Xml
2015-05-12 12:39:46 +10:00
{
/// <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
2015-05-12 12:39:46 +10:00
/// a real XPath statement
/// </summary>
2019-11-12 12:56:17 +11:00
public class UmbracoXPathPathSyntaxParser
2015-05-12 12:39:46 +10:00
{
/// <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 ,
2015-05-12 12:39:46 +10:00
Func < int , IEnumerable < string > > getPath ,
Func < int , bool > publishedContentExists )
{
2019-01-27 01:17:32 -05:00
// TODO: This should probably support some of the old syntax and token replacements, currently
2015-05-12 12:39:46 +10:00
// 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
2015-05-12 12:39:46 +10:00
// for discussion.
2019-10-07 22:10:21 +02:00
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 ) ) ;
2015-05-12 12:39:46 +10:00
//no need to parse it
if ( xpathExpression . StartsWith ( "$" ) = = false )
return xpathExpression ;
//get nearest published item
Func < IEnumerable < string > , int > getClosestPublishedAncestor = ( path = >
{
foreach ( var i in path )
{
int idAsInt ;
2021-09-15 13:40:08 +02:00
if ( int . TryParse ( i , NumberStyles . Integer , CultureInfo . InvariantCulture , out idAsInt ) )
2015-05-12 12:39:46 +10:00
{
2021-09-14 22:13:39 +02:00
var exists = publishedContentExists ( int . Parse ( i , CultureInfo . InvariantCulture ) ) ;
2015-05-12 12:39:46 +10:00
if ( exists )
return idAsInt ;
}
}
return - 1 ;
} ) ;
2017-09-18 15:33:13 +02:00
const string rootXpath = "id({0})" ;
2015-05-12 12:39:46 +10:00
//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
var path = getPath ( nodeContextId . Value ) . ToArray ( ) ;
if ( path [ 0 ] = = nodeContextId . ToString ( ) )
{
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]" ) ;
} ) ;
}
2019-01-27 01:17:32 -05:00
// TODO: This used to just replace $root with string.Empty BUT, that would never work
2015-05-12 12:39:46 +10:00
// 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 ;
}
}
}