From cf435011df439ec69f132c35d7c4781640ce9340 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 6 Sep 2013 22:05:16 +0200 Subject: [PATCH] Macros - support navigable caches --- src/Umbraco.Core/Umbraco.Core.csproj | 1 + src/Umbraco.Core/Xml/XPath/MacroNavigator.cs | 1042 +++++++++++++++++ .../CoreXml/NavigableNavigatorTests.cs | 137 +++ src/Umbraco.Web/Umbraco.Web.csproj | 2 +- src/Umbraco.Web/umbraco.presentation/macro.cs | 138 ++- 5 files changed, 1309 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Core/Xml/XPath/MacroNavigator.cs diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 88c532f8f2..b6e95a4c1b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -805,6 +805,7 @@ + diff --git a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs new file mode 100644 index 0000000000..501c8c3d1a --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs @@ -0,0 +1,1042 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml; +using System.Xml.XPath; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Provides a cursor model for navigating {macro /} as if it were XML. + /// + class MacroNavigator : XPathNavigator + { + private readonly XmlNameTable _nameTable; + private readonly MacroRoot _macro; + private State _state; + + #region Constructor + + /// + /// Initializes a new instance of the class with macro parameters. + /// + /// The macro parameters. + public MacroNavigator(IEnumerable parameters) + : this(new MacroRoot(parameters), new NameTable(), new State()) + { } + + /// + /// Initializes a new instance of the class with a macro node, + /// a name table and a state. + /// + /// The macro node. + /// The name table. + /// The state. + /// Privately used for cloning a navigator. + private MacroNavigator(MacroRoot macro, XmlNameTable nameTable, State state) + { + _macro = macro; + _nameTable = nameTable; + _state = state; + } + + #endregion + + #region Diagnostics + + // diagnostics code will not be compiled nor called into Release configuration. + // in Debug configuration, uncomment lines in Debug() to write to console or to log. + // + // much of this code is duplicated in each navigator due to conditional compilation + +#if DEBUG + private const string Tabs = " "; + private int _tabs; + private readonly int _uid = GetUid(); + private static int _uidg; + private readonly static object Uidl = new object(); + private static int GetUid() + { + lock (Uidl) + { + return _uidg++; + } + } +#endif + + [Conditional("DEBUG")] + void DebugEnter(string name) + { +#if DEBUG + Debug(""); + DebugState(":"); + Debug(name); + _tabs = Math.Min(Tabs.Length, _tabs + 2); +#endif + } + + [Conditional("DEBUG")] + void DebugCreate(MacroNavigator nav) + { +#if DEBUG + Debug("Create: [MacroNavigator::{0}]", nav._uid); +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn() + { +#if DEBUG +// ReSharper disable IntroduceOptionalParameters.Local + DebugReturn("(void)"); +// ReSharper restore IntroduceOptionalParameters.Local +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn(bool value) + { +#if DEBUG + DebugReturn(value ? "true" : "false"); +#endif + } + + [Conditional("DEBUG")] + void DebugReturn(string format, params object[] args) + { +#if DEBUG + Debug("=> " + format, args); + if (_tabs > 0) _tabs -= 2; +#endif + } + + [Conditional("DEBUG")] + void DebugState(string s = " =>") + { +#if DEBUG + string position; + + switch (_state.Position) + { + case StatePosition.Macro: + position = "At macro."; + break; + case StatePosition.Parameter: + position = string.Format("At parameter '{0}'.", + _macro.Parameters[_state.ParameterIndex].Name); + break; + case StatePosition.ParameterAttribute: + position = string.Format("At parameter attribute '{0}/{1}'.", + _macro.Parameters[_state.ParameterIndex].Name, + _macro.Parameters[_state.ParameterIndex].Attributes[_state.ParameterAttributeIndex].Key); + break; + case StatePosition.ParameterNavigator: + position = string.Format("In parameter '{0}{1}' navigator.", + _macro.Parameters[_state.ParameterIndex].Name, + _macro.Parameters[_state.ParameterIndex].WrapNavigatorInNodes ? "/nodes" : ""); + break; + case StatePosition.ParameterNodes: + position = string.Format("At parameter '{0}/nodes'.", + _macro.Parameters[_state.ParameterIndex].Name); + break; + case StatePosition.ParameterText: + position = string.Format("In parameter '{0}' text.", + _macro.Parameters[_state.ParameterIndex].Name); + break; + case StatePosition.Root: + position = "At root."; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + Debug("State{0} {1}", s, position); +#endif + } + +#if DEBUG + void Debug(string format, params object[] args) + { + // remove comments to write + + format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format; +#pragma warning disable 168 + var msg = string.Format(format, args); // unused if not writing, hence #pragma +#pragma warning restore 168 + //LogHelper.Debug(msg); // beware! this can quicky overflow log4net + //Console.WriteLine(msg); + } +#endif + + #endregion + + #region Macro + + private class MacroRoot + { + public MacroRoot(IEnumerable parameters) + { + Parameters = parameters == null ? new MacroParameter[] {} : parameters.ToArray(); + } + + public MacroParameter[] Parameters { get; private set; } + } + + public class MacroParameter + { + // note: assuming we're not thinking about supporting + // XPathIterator in parameters - enough nonsense! + + public MacroParameter(string name, string value) + { + Name = name; + StringValue = value; + } + + public MacroParameter(string name, XPathNavigator navigator, + int maxNavigatorDepth = int.MaxValue, + bool wrapNavigatorInNodes = false, + IEnumerable> attributes = null) + { + Name = name; + MaxNavigatorDepth = maxNavigatorDepth; + WrapNavigatorInNodes = wrapNavigatorInNodes; + if (attributes != null) + { + var a = attributes.ToArray(); + if (a.Length > 0) + Attributes = a; + } + NavigatorValue = navigator; // should not be empty + } + + public string Name { get; private set; } + public string StringValue { get; private set; } + public XPathNavigator NavigatorValue { get; private set; } + public int MaxNavigatorDepth { get; private set; } + public bool WrapNavigatorInNodes { get; private set; } + public KeyValuePair[] Attributes { get; private set; } + } + + #endregion + + /// + /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. + /// + /// A new XPathNavigator positioned at the same node as this XPathNavigator. + public override XPathNavigator Clone() + { + DebugEnter("Clone"); + var nav = new MacroNavigator(_macro, _nameTable, _state.Clone()); + DebugCreate(nav); + DebugReturn("[XPathNavigator]"); + return nav; + } + + /// + /// Gets a value indicating whether the current node is an empty element without an end element tag. + /// + public override bool IsEmptyElement + { + get + { + DebugEnter("IsEmptyElement"); + bool isEmpty; + + switch (_state.Position) + { + case StatePosition.Macro: + isEmpty = _macro.Parameters.Length == 0; + break; + case StatePosition.Parameter: + var parameter = _macro.Parameters[_state.ParameterIndex]; + var nav = parameter.NavigatorValue; + if (parameter.WrapNavigatorInNodes || nav != null) + { + isEmpty = false; + } + else + { + var s = _macro.Parameters[_state.ParameterIndex].StringValue; + isEmpty = s == null; + } + break; + case StatePosition.ParameterNavigator: + isEmpty = _state.ParameterNavigator.IsEmptyElement; + break; + case StatePosition.ParameterNodes: + isEmpty = _macro.Parameters[_state.ParameterIndex].NavigatorValue == null; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterText: + case StatePosition.Root: + throw new InvalidOperationException("Not an element."); + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isEmpty); + return isEmpty; + } + } + + /// + /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. + /// + /// The XPathNavigator to compare to this XPathNavigator. + /// true if the two XPathNavigator objects have the same position; otherwise, false. + public override bool IsSamePosition(XPathNavigator nav) + { + DebugEnter("IsSamePosition"); + bool isSame; + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + case StatePosition.Macro: + case StatePosition.Parameter: + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.ParameterText: + case StatePosition.Root: + var other = nav as MacroNavigator; + isSame = other != null && other._macro == _macro && _state.IsSamePosition(other._state); + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isSame); + return isSame; + } + + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get + { + DebugEnter("Name"); + string name; + + switch (_state.Position) + { + case StatePosition.Macro: + name = "macro"; + break; + case StatePosition.Parameter: + name = _macro.Parameters[_state.ParameterIndex].Name; + break; + case StatePosition.ParameterAttribute: + name = _macro.Parameters[_state.ParameterIndex].Attributes[_state.ParameterAttributeIndex].Key; + break; + case StatePosition.ParameterNavigator: + name = _state.ParameterNavigator.Name; + break; + case StatePosition.ParameterNodes: + name = "nodes"; + break; + case StatePosition.ParameterText: + case StatePosition.Root: + name = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get + { + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Moves the XPathNavigator to the same position as the specified XPathNavigator. + /// + /// The XPathNavigator positioned on the node that you want to move to. + /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveTo(XPathNavigator nav) + { + DebugEnter("MoveTo"); + + var other = nav as MacroNavigator; + var succ = false; + + if (other != null && other._macro == _macro) + { + _state = other._state.Clone(); + DebugState(); + succ = true; + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first attribute of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstAttribute() + { + DebugEnter("MoveToFirstAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + succ = _state.ParameterNavigator.MoveToFirstAttribute(); + break; + case StatePosition.Parameter: + if (_macro.Parameters[_state.ParameterIndex].Attributes != null) + { + _state.Position = StatePosition.ParameterAttribute; + _state.ParameterAttributeIndex = 0; + succ = true; + DebugState(); + } + else succ = false; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.Macro: + case StatePosition.ParameterText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first child node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstChild() + { + DebugEnter("MoveToFirstChild"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Macro: + if (_macro.Parameters.Length == 0) + { + succ = false; + } + else + { + _state.ParameterIndex = 0; + _state.Position = StatePosition.Parameter; + succ = true; + } + break; + case StatePosition.Parameter: + var parameter = _macro.Parameters[_state.ParameterIndex]; + var nav = parameter.NavigatorValue; + if (parameter.WrapNavigatorInNodes) + { + _state.Position = StatePosition.ParameterNodes; + DebugState(); + succ = true; + } + else if (nav != null) + { + nav = nav.Clone(); // never use the raw parameter's navigator + nav.MoveToFirstChild(); + _state.ParameterNavigator = nav; + _state.ParameterNavigatorDepth = 0; + _state.Position = StatePosition.ParameterNavigator; + DebugState(); + succ = true; + } + else + { + var s = _macro.Parameters[_state.ParameterIndex].StringValue; + if (s != null) + { + _state.Position = StatePosition.ParameterText; + DebugState(); + succ = true; + } + else succ = false; + } + break; + case StatePosition.ParameterNavigator: + if (_state.ParameterNavigatorDepth == _macro.Parameters[_state.ParameterIndex].MaxNavigatorDepth) + { + succ = false; + } + else + { + // move to first doc child => increment depth, else (property child) do nothing + succ = _state.ParameterNavigator.MoveToFirstChild(); + if (succ && IsDoc(_state.ParameterNavigator)) + { + ++_state.ParameterNavigatorDepth; + DebugState(); + } + } + break; + case StatePosition.ParameterNodes: + if (_macro.Parameters[_state.ParameterIndex].NavigatorValue != null) + { + // never use the raw parameter's navigator + _state.ParameterNavigator = _macro.Parameters[_state.ParameterIndex].NavigatorValue.Clone(); + _state.Position = StatePosition.ParameterNavigator; + succ = true; + DebugState(); + } + else succ = false; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterText: + succ = false; + break; + case StatePosition.Root: + _state.Position = StatePosition.Macro; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the first namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToFirstNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the next namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToNextNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves to the node that has an attribute of type ID whose value matches the specified String. + /// + /// A String representing the ID value of the node to which you want to move. + /// true if the XPathNavigator is successful moving; otherwise, false. + /// If false, the position of the navigator is unchanged. + public override bool MoveToId(string id) + { + DebugEnter("MoveToId"); + // impossible to implement since parameters can contain duplicate fragments of the + // main xml and therefore there can be duplicate unique node identifiers. + DebugReturn("NotImplementedException"); + throw new NotImplementedException(); + } + + /// + /// Moves the XPathNavigator to the next sibling node of the current node. + /// + /// true if the XPathNavigator is successful moving to the next sibling node; + /// otherwise, false if there are no more siblings or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNext() + { + DebugEnter("MoveToNext"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Parameter: + if (_state.ParameterIndex == _macro.Parameters.Length - 1) + { + succ = false; + } + else + { + ++_state.ParameterIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.ParameterNavigator: + var wasDoc = IsDoc(_state.ParameterNavigator); + succ = _state.ParameterNavigator.MoveToNext(); + if (succ && !wasDoc && IsDoc(_state.ParameterNavigator)) + { + // move to first doc child => increment depth, else (another property child) do nothing + if (_state.ParameterNavigatorDepth == _macro.Parameters[_state.ParameterIndex].MaxNavigatorDepth) + { + _state.ParameterNavigator.MoveToPrevious(); + succ = false; + } + else + { + ++_state.ParameterNavigatorDepth; + DebugState(); + } + } + break; + case StatePosition.ParameterNodes: + case StatePosition.ParameterAttribute: + case StatePosition.ParameterText: + case StatePosition.Macro: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the previous sibling node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the previous sibling node; + /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToPrevious() + { + DebugEnter("MoveToPrevious"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Parameter: + if (_state.ParameterIndex == -1) + { + succ = false; + } + else + { + --_state.ParameterIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.ParameterNavigator: + var wasDoc = IsDoc(_state.ParameterNavigator); + succ = _state.ParameterNavigator.MoveToPrevious(); + if (succ && wasDoc && !IsDoc(_state.ParameterNavigator)) + { + // move from doc child back to property child => decrement depth + --_state.ParameterNavigatorDepth; + DebugState(); + } + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.ParameterText: + case StatePosition.Macro: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next attribute. + /// + /// Returns true if the XPathNavigator is successful moving to the next attribute; + /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextAttribute() + { + DebugEnter("MoveToNextAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + succ = _state.ParameterNavigator.MoveToNextAttribute(); + break; + case StatePosition.ParameterAttribute: + if (_state.ParameterAttributeIndex == _macro.Parameters[_state.ParameterIndex].Attributes.Length - 1) + succ = false; + else + { + ++_state.ParameterAttributeIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.Parameter: + case StatePosition.ParameterNodes: + case StatePosition.ParameterText: + case StatePosition.Macro: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the parent node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToParent() + { + DebugEnter("MoveToParent"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Macro: + _state.Position = StatePosition.Root; + DebugState(); + succ = true; + break; + case StatePosition.Parameter: + _state.Position = StatePosition.Macro; + DebugState(); + succ = true; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + _state.Position = StatePosition.Parameter; + DebugState(); + succ = true; + break; + case StatePosition.ParameterNavigator: + var wasDoc = IsDoc(_state.ParameterNavigator); + succ = _state.ParameterNavigator.MoveToParent(); + if (succ) + { + // move from doc child => decrement depth + if (wasDoc && --_state.ParameterNavigatorDepth == 0) + { + _state.Position = StatePosition.Parameter; + _state.ParameterNavigator = null; + DebugState(); + } + } + break; + case StatePosition.ParameterText: + _state.Position = StatePosition.Parameter; + DebugState(); + succ = true; + break; + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the root node that the current node belongs to. + /// + public override void MoveToRoot() + { + DebugEnter("MoveToRoot"); + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + _state.ParameterNavigator = null; + _state.ParameterNavigatorDepth = -1; + break; + case StatePosition.Parameter: + case StatePosition.ParameterText: + _state.ParameterIndex = -1; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.Macro: + case StatePosition.Root: + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + _state.Position = StatePosition.Root; + DebugState(); + + DebugReturn(); + } + + /// + /// Gets the base URI for the current node. + /// + public override string BaseURI + { + get { return string.Empty; } + } + + /// + /// Gets the XmlNameTable of the XPathNavigator. + /// + public override XmlNameTable NameTable + { + get { return _nameTable; } + } + + /// + /// Gets the namespace URI of the current node. + /// + public override string NamespaceURI + { + get { return string.Empty; } + } + + /// + /// Gets the XPathNodeType of the current node. + /// + public override XPathNodeType NodeType + { + get + { + DebugEnter("NodeType"); + XPathNodeType type; + + switch (_state.Position) + { + case StatePosition.Macro: + case StatePosition.Parameter: + case StatePosition.ParameterNodes: + type = XPathNodeType.Element; + break; + case StatePosition.ParameterNavigator: + type = _state.ParameterNavigator.NodeType; + break; + case StatePosition.ParameterAttribute: + type = XPathNodeType.Attribute; + break; + case StatePosition.ParameterText: + type = XPathNodeType.Text; + break; + case StatePosition.Root: + type = XPathNodeType.Root; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\'{0}\'", type); + return type; + } + } + + /// + /// Gets the namespace prefix associated with the current node. + /// + public override string Prefix + { + get { return string.Empty; } + } + + /// + /// Gets the string value of the item. + /// + /// Does not fully behave as per the specs, as we report empty value on root and macro elements, and we start + /// reporting values only on parameter elements. This is because, otherwise, we would might dump the whole database + /// and it probably does not make sense at Umbraco level. + public override string Value + { + get + { + DebugEnter("Value"); + string value; + + XPathNavigator nav; + switch (_state.Position) + { + case StatePosition.Parameter: + nav = _macro.Parameters[_state.ParameterIndex].NavigatorValue; + if (nav != null) + { + nav = nav.Clone(); // never use the raw parameter's navigator + nav.MoveToFirstChild(); + value = nav.Value; + } + else + { + var s = _macro.Parameters[_state.ParameterIndex].StringValue; + value = s ?? string.Empty; + } + break; + case StatePosition.ParameterAttribute: + value = _macro.Parameters[_state.ParameterIndex].Attributes[_state.ParameterAttributeIndex].Value; + break; + case StatePosition.ParameterNavigator: + value = _state.ParameterNavigator.Value; + break; + case StatePosition.ParameterNodes: + nav = _macro.Parameters[_state.ParameterIndex].NavigatorValue; + if (nav == null) + value = string.Empty; + else + { + nav = nav.Clone(); // never use the raw parameter's navigator + nav.MoveToFirstChild(); + value = nav.Value; + } + break; + case StatePosition.ParameterText: + value = _macro.Parameters[_state.ParameterIndex].StringValue; + break; + case StatePosition.Macro: + case StatePosition.Root: + value = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", value); + return value; + } + } + + private static bool IsDoc(XPathNavigator nav) + { + if (nav.NodeType != XPathNodeType.Element) + return false; + + var clone = nav.Clone(); + if (!clone.MoveToFirstAttribute()) + return false; + do + { + if (clone.Name == "isDoc") + return true; + } while (clone.MoveToNextAttribute()); + + return false; + } + + #region State management + + // the possible state positions + internal enum StatePosition + { + Root, + Macro, + Parameter, + ParameterAttribute, + ParameterText, + ParameterNodes, + ParameterNavigator + }; + + // gets the state + // for unit tests only + internal State InternalState { get { return _state; } } + + // represents the XPathNavigator state + internal class State + { + public StatePosition Position { get; set; } + + // initialize a new state + private State(StatePosition position) + { + Position = position; + ParameterIndex = 0; + ParameterNavigatorDepth = 0; + ParameterAttributeIndex = 0; + } + + // initialize a new state + // used for creating the very first state + public State() + : this(StatePosition.Root) + { } + + // initialize a clone state + private State(State other) + { + Position = other.Position; + + ParameterIndex = other.ParameterIndex; + + if (Position == StatePosition.ParameterNavigator) + { + ParameterNavigator = other.ParameterNavigator.Clone(); + ParameterNavigatorDepth = other.ParameterNavigatorDepth; + ParameterAttributeIndex = other.ParameterAttributeIndex; + } + } + + public State Clone() + { + return new State(this); + } + + // the index of the current element + public int ParameterIndex { get; set; } + + // the current depth within the element navigator + public int ParameterNavigatorDepth { get; set; } + + // the index of the current element's attribute + public int ParameterAttributeIndex { get; set; } + + // gets or sets the element navigator + public XPathNavigator ParameterNavigator { get; set; } + + // gets a value indicating whether this state is at the same position as another one. + public bool IsSamePosition(State other) + { + return other.Position == Position + && (Position != StatePosition.ParameterNavigator || other.ParameterNavigator.IsSamePosition(ParameterNavigator)) + && other.ParameterIndex == ParameterIndex + && other.ParameterAttributeIndex == ParameterAttributeIndex; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs index c14894bf43..a23a6f6435 100644 --- a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs +++ b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs @@ -594,6 +594,143 @@ namespace Umbraco.Tests.CoreXml Assert.IsFalse(nav.MoveToId("2")); } + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void XsltDebugModeAndSortOrder(bool native, bool debug) + { + const string xml = @" + + + title-1 + + title-3 + + title-7 + + + title-8 + + + + title-5 + + + + title-2 + + title-4 + + + title-6 + + + +"; + + const string xslt = @" + +]> + + + + + + + + + +! + + +!! + + +!!! + + + + + + + +"; + const string expected = @"! title-1 +!! title-3 +!!! title-7 +!!! title-8 +!! title-5 +! title-2 +!! title-4 +!! title-6 +"; + + // see http://www.onenaught.com/posts/352/xslt-performance-tip-dont-indent-output + // why aren't we using an XmlWriter here? + + var transform = new XslCompiledTransform(debug); + var xmlReader = new XmlTextReader(new StringReader(xslt)) + { + EntityHandling = EntityHandling.ExpandEntities + }; + var xslResolver = new XmlUrlResolver + { + Credentials = CredentialCache.DefaultCredentials + }; + var args = new XsltArgumentList(); + + // .Default is more restrictive than .TrustedXslt + transform.Load(xmlReader, XsltSettings.Default, xslResolver); + + XPathNavigator macro; + if (!native) + { + var source = new TestSource7(); + var nav = new NavigableNavigator(source); + //args.AddParam("currentPage", string.Empty, nav.Clone()); + + var x = new XmlDocument(); + x.LoadXml(xml); + + macro = new MacroNavigator(new[] + { + // it even fails like that => macro nav. issue? + new MacroNavigator.MacroParameter("nav", x.CreateNavigator()) // nav.Clone()) + } + ); + } + else + { + var doc = new XmlDocument(); + doc.LoadXml(""); + var nav = doc.CreateElement("nav"); + doc.DocumentElement.AppendChild(nav); + var x = new XmlDocument(); + x.LoadXml(xml); + nav.AppendChild(doc.ImportNode(x.DocumentElement, true)); + macro = doc.CreateNavigator(); + } + + var writer = new StringWriter(); + transform.Transform(macro, args, writer); + + // this was working with native, debug and non-debug + // this was working with macro nav, non-debug + // but was NOT working (changing the order of nodes) with macro nav, debug + // was due to an issue with macro nav IsSamePosition, fixed + + //Console.WriteLine("--------"); + //Console.WriteLine(writer.ToString()); + Assert.AreEqual(expected, writer.ToString()); + } + [Test] public void WhiteSpacesAndEmptyValues() { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6ad8ce6049..dc9d11b478 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -2085,4 +2085,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index a457ceac72..b8f94ce7b6 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -23,6 +23,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Xml.XPath; using Umbraco.Core.Profiling; using Umbraco.Web; using Umbraco.Web.Cache; @@ -859,21 +860,41 @@ namespace umbraco using (DisposableTimer.DebugDuration("Executing XSLT: " + XsltFile)) { XmlDocument macroXml = null; + MacroNavigator macroNavigator = null; + NavigableNavigator contentNavigator = null; - // get master xml document - var cache = UmbracoContext.Current.ContentCache.InnerCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; - if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); - XmlDocument umbracoXml = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); - macroXml = new XmlDocument(); - macroXml.LoadXml(""); - foreach (var prop in macro.Model.Properties) + var canNavigate = + UmbracoContext.Current.ContentCache.XPathNavigatorIsNavigable && + UmbracoContext.Current.MediaCache.XPathNavigatorIsNavigable; + + if (!canNavigate) { - AddMacroXmlNode(umbracoXml, macroXml, prop.Key, prop.Type, prop.Value); + // get master xml document + var cache = UmbracoContext.Current.ContentCache.InnerCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + XmlDocument umbracoXml = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); + macroXml = new XmlDocument(); + macroXml.LoadXml(""); + foreach (var prop in macro.Model.Properties) + { + AddMacroXmlNode(umbracoXml, macroXml, prop.Key, prop.Type, prop.Value); + } + } + else + { + var parameters = new List(); + contentNavigator = UmbracoContext.Current.ContentCache.GetXPathNavigator() as NavigableNavigator; + var mediaNavigator = UmbracoContext.Current.MediaCache.GetXPathNavigator() as NavigableNavigator; + foreach (var prop in macro.Model.Properties) + { + AddMacroParameter(parameters, contentNavigator, mediaNavigator, prop.Key, prop.Type, prop.Value); + } + macroNavigator = new MacroNavigator(parameters); } if (HttpContext.Current.Request.QueryString["umbDebug"] != null && GlobalSettings.DebugMode) { - var outerXml = macroXml.OuterXml; + var outerXml = macroXml == null ? macroNavigator.OuterXml : macroXml.OuterXml; return new LiteralControl("
Debug from " + macro.Name + @@ -889,7 +910,9 @@ namespace umbraco { try { - var transformed = GetXsltTransformResult(macroXml, xsltFile); + var transformed = canNavigate + ? GetXsltTransformResult(macroNavigator, contentNavigator, xsltFile) // better? + : GetXsltTransformResult(macroXml, xsltFile); // document var result = CreateControlsFromText(transformed); return result; @@ -1323,6 +1346,101 @@ namespace umbraco macroXml.FirstChild.AppendChild(macroXmlNode); } + // add parameters to the macro parameters collection + private void AddMacroParameter(ICollection parameters, + NavigableNavigator contentNavigator, NavigableNavigator mediaNavigator, + string macroPropertyAlias,string macroPropertyType, string macroPropertyValue) + { + // if no value is passed, then use the current "pageID" as value + var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; + + TraceInfo("umbracoMacro", + "Xslt node adding search start (" + macroPropertyAlias + ",'" + + macroPropertyValue + "')"); + + // beware! do not use the raw content- or media- navigators, but clones !! + + switch (macroPropertyType) + { + case "contentTree": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + attributes: new Dictionary { { "nodeID", contentId } })); + + break; + + case "contentPicker": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 0)); + break; + + case "contentSubs": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 1)); + break; + + case "contentAll": + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, contentNavigator.Clone())); + break; + + case "contentRandom": + var nav = contentNavigator.Clone(); + if (nav.MoveToId(contentId)) + { + var descendantIterator = nav.Select("./* [@isDoc]"); + if (descendantIterator.MoveNext()) + { + // not empty - and won't change + var descendantCount = descendantIterator.Count; + + int index; + var r = library.GetRandom(); + lock (r) + { + index = r.Next(descendantCount); + } + + while (index > 0 && descendantIterator.MoveNext()) + index--; + + var node = descendantIterator.Current.UnderlyingObject as INavigableContent; + if (node != null) + { + nav = contentNavigator.CloneWithNewRoot(node.Id.ToString(CultureInfo.InvariantCulture)); + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, nav, 0)); + } + else + throw new InvalidOperationException("Iterator contains non-INavigableContent elements."); + } + else + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't have children!"); + } + else + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't exists!"); + break; + + case "mediaCurrent": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + mediaNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 0)); + break; + + default: + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, HttpContext.Current.Server.HtmlDecode(macroPropertyValue))); + break; + } + } + #endregion ///