diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 2cc6a8fbd4..88c532f8f2 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -805,6 +805,11 @@
+
+
+
+
+
diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs
new file mode 100644
index 0000000000..d8b6adf00a
--- /dev/null
+++ b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Xml.XPath
+{
+ ///
+ /// Represents a content that can be navigated via XPath.
+ ///
+ interface INavigableContent
+ {
+ ///
+ /// Gets the unique identifier of the navigable content.
+ ///
+ /// The root node identifier should be -1 .
+ int Id { get; }
+
+ ///
+ /// Gets the unique identifier of parent of the navigable content.
+ ///
+ /// The top-level content parent identifiers should be -1 ie the identifier
+ /// of the root node, whose parent identifier should in turn be -1 .
+ int ParentId { get; }
+
+ ///
+ /// Gets the type of the navigable content.
+ ///
+ INavigableContentType Type { get; }
+
+ ///
+ /// Gets the unique identifiers of the children of the navigable content.
+ ///
+ IList ChildIds { get; }
+
+ ///
+ /// Gets the value of a field of the navigable content for XPath navigation use.
+ ///
+ /// The field index.
+ /// The value of the field for XPath navigation use.
+ ///
+ /// Fields are attributes or elements depending on their relative index value compared
+ /// to source.LastAttributeIndex.
+ /// For attributes, the value must be a string.
+ /// For elements, the value should an XPathNavigator instance if the field is xml
+ /// and has content (is not empty), null to indicate that the element is empty, or a string
+ /// which can be empty, whitespace... depending on what the data type wants to expose.
+ ///
+ object Value(int index);
+
+ // TODO implement the following one
+
+ /////
+ ///// Gets the value of a field of the navigable content, for a specified language.
+ /////
+ ///// The field index.
+ ///// The language key.
+ ///// The value of the field for the specified language.
+ ///// ...
+ //object Value(int index, string languageKey);
+ }
+}
diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs
new file mode 100644
index 0000000000..94b225467c
--- /dev/null
+++ b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Umbraco.Core.Xml.XPath
+{
+ ///
+ /// Represents the type of a content that can be navigated via XPath.
+ ///
+ interface INavigableContentType
+ {
+ ///
+ /// Gets the name of the content type.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets the field types of the content type.
+ ///
+ /// This includes the attributes and the properties.
+ INavigableFieldType[] FieldTypes { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs
new file mode 100644
index 0000000000..32a6f64751
--- /dev/null
+++ b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Umbraco.Core.Xml.XPath
+{
+ ///
+ /// Represents the type of a field of a content that can be navigated via XPath.
+ ///
+ /// A field can be an attribute or a property.
+ interface INavigableFieldType
+ {
+ ///
+ /// Gets the name of the field type.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets a method to convert the field value to a string.
+ ///
+ /// This is for built-in properties, ie attributes. User-defined properties have their
+ /// own way to convert their value for XPath.
+ Func XmlStringConverter { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs
new file mode 100644
index 0000000000..68dc9e906a
--- /dev/null
+++ b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Umbraco.Core.Xml.XPath
+{
+ ///
+ /// Represents a source of content that can be navigated via XPath.
+ ///
+ interface INavigableSource
+ {
+ ///
+ /// Gets a content identified by its unique identifier.
+ ///
+ /// The unique identifier.
+ /// The content identified by the unique identifier, or null.
+ /// When id is -1 (root content) implementations should return null .
+ INavigableContent Get(int id);
+
+ ///
+ /// Gets the index of the last attribute in the fields collections.
+ ///
+ int LastAttributeIndex { get; }
+
+ ///
+ /// Gets the content at the root of the source.
+ ///
+ /// That content should have unique identifier -1 and should not be gettable,
+ /// ie Get(-1) should return null. Its ParentId should be -1 . It should provide
+ /// values for the attribute fields.
+ INavigableContent Root { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs
new file mode 100644
index 0000000000..a17727d4ac
--- /dev/null
+++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs
@@ -0,0 +1,1149 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Xml;
+using System.Xml.XPath;
+
+namespace Umbraco.Core.Xml.XPath
+{
+ ///
+ /// Provides a cursor model for navigating Umbraco data as if it were XML.
+ ///
+ class NavigableNavigator : XPathNavigator
+ {
+ // "The XmlNameTable stores atomized strings of any local name, namespace URI,
+ // and prefix used by the XPathNavigator. This means that when the same Name is
+ // returned multiple times (like "book"), the same String object is returned for
+ // that Name. This makes it possible to write efficient code that does object
+ // comparisons on these strings, instead of expensive string comparisons."
+ //
+ // "When an element or attribute name occurs multiple times in an XML document,
+ // it is stored only once in the NameTable. The names are stored as common
+ // language runtime (CLR) object types. This enables you to do object comparisons
+ // on these strings rather than a more expensive string comparison. These
+ // string objects are referred to as atomized strings."
+ //
+ // But... "Any instance members are not guaranteed to be thread safe."
+ //
+ // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx
+ // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx
+ // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx
+ //
+ // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to
+ // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI,
+ // and Prefix properties are returned, the string returned should come from the
+ // NameTable. Comparisons between names are done by object comparisons rather
+ // than by string comparisons, which are significantly slower.""
+ //
+ // So what shall we do? Well, here we have no namespace, no prefix, and all
+ // local names come from cached instances of INavigableContentType or
+ // INavigableFieldType and are already unique. So... create a one nametable
+ // because we need one, and share it amongst all clones.
+
+ private readonly XmlNameTable _nameTable;
+ private readonly INavigableSource _source;
+ private readonly int _lastAttributeIndex; // last index of attributes in the fields collection
+ private State _state;
+
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class with a content source.
+ ///
+ private NavigableNavigator(INavigableSource source)
+ {
+ _source = source;
+ _lastAttributeIndex = source.LastAttributeIndex;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a content source,
+ /// and an optional root content.
+ ///
+ /// The content source.
+ /// The root content.
+ /// When no root content is supplied then the root of the source is used.
+ public NavigableNavigator(INavigableSource source, INavigableContent content = null)
+ : this(source)
+ {
+ _nameTable = new NameTable();
+ _lastAttributeIndex = source.LastAttributeIndex;
+ _state = new State(content ?? source.Root, null, null, 0, StatePosition.Root);
+ }
+
+ ///
+ /// Initializes a new instance of the class with a content source, a name table and a state.
+ ///
+ /// The content source.
+ /// The name table.
+ /// The state.
+ /// Privately used for cloning a navigator.
+ private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state)
+ : this(source)
+ {
+ _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.
+
+#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(NavigableNavigator nav)
+ {
+#if DEBUG
+ Debug("Create: [NavigableNavigator::{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.Attribute:
+ position = string.Format("At attribute '{0}/@{1}'.",
+ _state.Content.Type.Name,
+ _state.FieldIndex < 0 ? "id" : _state.CurrentFieldType.Name);
+ break;
+ case StatePosition.Element:
+ position = string.Format("At element '{0}'.",
+ _state.Content.Type.Name);
+ break;
+ case StatePosition.PropertyElement:
+ position = string.Format("At property '{0}/{1}'.",
+ _state.Content.Type.Name, _state.Content.Type.FieldTypes[this._state.FieldIndex].Name);
+ break;
+ case StatePosition.PropertyText:
+ position = string.Format("At property '{0}/{1}' text.",
+ _state.Content.Type.Name, _state.CurrentFieldType.Name);
+ break;
+ case StatePosition.PropertyXml:
+ position = string.Format("In property '{0}/{1}' xml fragment.",
+ _state.Content.Type.Name, _state.CurrentFieldType.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
+
+ ///
+ /// Gets the underlying content object.
+ ///
+ public override object UnderlyingObject
+ {
+ get { return _state.Content; }
+ }
+
+ ///
+ /// 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 NavigableNavigator(_source, _nameTable, _state.Clone());
+ DebugCreate(nav);
+ DebugReturn("[XPathNavigator]");
+ return nav;
+ }
+
+ ///
+ /// Creates a new XPathNavigator using the same source but positioned at a new root.
+ ///
+ /// A new XPathNavigator using the same source and positioned at a new root.
+ /// The new root can be above this navigator's root.
+ public XPathNavigator CloneWithNewRoot(string id)
+ {
+ DebugEnter("CloneWithNewRoot");
+
+ int contentId;
+ State state = null;
+
+ if (id != null && id.Trim() == "-1")
+ {
+ state = new State(_source.Root, null, null, 0, StatePosition.Root);
+ }
+ else if (int.TryParse(id, out contentId))
+ {
+ var content = _source.Get(contentId);
+ if (content != null)
+ {
+ state = new State(content, null, null, 0, StatePosition.Root);
+ }
+ }
+
+ NavigableNavigator clone = null;
+
+ if (state != null)
+ {
+ clone = new NavigableNavigator(_source, _nameTable, state);
+ DebugCreate(clone);
+ DebugReturn("[XPathNavigator]");
+ }
+ else
+ {
+ DebugReturn("[null]");
+ }
+
+ return clone;
+ }
+
+ ///
+ /// 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.Element:
+ isEmpty = (_state.Content.ChildIds == null || _state.Content.ChildIds.Count == 0) // no content child
+ && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child
+ break;
+ case StatePosition.PropertyElement:
+ // value should be
+ // - an XPathNavigator over a non-empty XML fragment
+ // - a non-Xml-whitespace string
+ // - null
+ isEmpty = _state.Content.Value(_state.FieldIndex) == null;
+ break;
+ case StatePosition.PropertyXml:
+ isEmpty = _state.XmlFragmentNavigator.IsEmptyElement;
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.PropertyText:
+ 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.PropertyXml:
+ isSame = _state.XmlFragmentNavigator.IsSamePosition(nav);
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.Element:
+ case StatePosition.PropertyElement:
+ case StatePosition.PropertyText:
+ case StatePosition.Root:
+ var other = nav as NavigableNavigator;
+ isSame = other != null && other._source == _source && _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.PropertyXml:
+ name = _state.XmlFragmentNavigator.Name;
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.PropertyElement:
+ name = _state.FieldIndex == -1 ? "id" : _state.CurrentFieldType.Name;
+ break;
+ case StatePosition.Element:
+ name = _state.Content.Type.Name;
+ break;
+ case StatePosition.PropertyText:
+ name = string.Empty;
+ break;
+ 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 NavigableNavigator;
+ var succ = false;
+
+ if (other != null && other._source == _source)
+ {
+ _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.PropertyXml:
+ succ = _state.XmlFragmentNavigator.MoveToFirstAttribute();
+ break;
+ case StatePosition.Element:
+ _state.FieldIndex = -1;
+ _state.Position = StatePosition.Attribute;
+ DebugState();
+ succ = true;
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.PropertyElement:
+ case StatePosition.PropertyText:
+ 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.PropertyXml:
+ succ = _state.XmlFragmentNavigator.MoveToFirstChild();
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.PropertyText:
+ succ = false;
+ break;
+ case StatePosition.Element:
+ var firstPropertyIndex = _lastAttributeIndex + 1;
+ if (_state.FieldsCount > firstPropertyIndex)
+ {
+ _state.Position = StatePosition.PropertyElement;
+ _state.FieldIndex = firstPropertyIndex;
+ DebugState();
+ succ = true;
+ }
+ else succ = MoveToFirstChildElement();
+ break;
+ case StatePosition.PropertyElement:
+ succ = MoveToFirstChildProperty();
+ break;
+ case StatePosition.Root:
+ _state.Position = StatePosition.Element;
+ DebugState();
+ succ = true;
+ break;
+ default:
+ throw new InvalidOperationException("Invalid position.");
+ }
+
+ DebugReturn(succ);
+ return succ;
+ }
+
+ private bool MoveToFirstChildElement()
+ {
+ var children = _state.Content.ChildIds;
+
+ if (children != null && children.Count > 0)
+ {
+ // children may contain IDs that does not correspond to some content in source
+ // because children contains all child IDs including unpublished children - and
+ // then if we're not previewing, the source will return null.
+ var child = children.Select(id => _source.Get(id)).FirstOrDefault(c => c != null);
+ if (child != null)
+ {
+ _state.Position = StatePosition.Element;
+ _state.FieldIndex = -1;
+ _state = new State(child, _state, children, 0, StatePosition.Element);
+ DebugState();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool MoveToFirstChildProperty()
+ {
+ var valueForXPath = _state.Content.Value(_state.FieldIndex);
+
+ // value should be
+ // - an XPathNavigator over a non-empty XML fragment
+ // - a non-Xml-whitespace string
+ // - null
+
+ var nav = valueForXPath as XPathNavigator;
+ if (nav != null)
+ {
+ nav = nav.Clone(); // never use the one we got
+ nav.MoveToFirstChild();
+ _state.XmlFragmentNavigator = nav;
+ _state.Position = StatePosition.PropertyXml;
+ DebugState();
+ return true;
+ }
+
+ if (valueForXPath == null)
+ return false;
+
+ if (valueForXPath is string)
+ {
+ _state.Position = StatePosition.PropertyText;
+ DebugState();
+ return true;
+ }
+
+ throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string.");
+ }
+
+ ///
+ /// 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");
+ var succ = false;
+
+ // don't look into fragments, just look for element identifiers
+ // not sure we actually need to implement it... think of it as
+ // as exercise of style, always better than throwing NotImplemented.
+
+ int contentId;
+ if (/*id != null &&*/ id.Trim() == "-1") // id cannot be null
+ {
+ _state = new State(_source.Root, null, _source.Root.ChildIds, 0, StatePosition.Element);
+ succ = true;
+ }
+ else if (int.TryParse(id, out contentId))
+ {
+ var content = _source.Get(contentId);
+ if (content != null)
+ {
+ var state = _state;
+ while (state.Parent != null)
+ state = state.Parent;
+ var navRootId = state.Content.Id; // navigator may be rooted below source root
+
+ var s = new Stack();
+ while (content != null && content.ParentId != navRootId)
+ {
+ s.Push(content);
+ content = _source.Get(content.ParentId);
+ }
+ if (content != null)
+ {
+ _state = new State(_source.Root, null, _source.Root.ChildIds, _source.Root.ChildIds.IndexOf(content.Id), StatePosition.Element);
+ while (content != null)
+ {
+ _state = new State(content, _state, content.ChildIds, _state.Content.ChildIds.IndexOf(content.Id), StatePosition.Element);
+ content = s.Count == 0 ? null : s.Pop();
+ }
+ DebugState();
+ succ = true;
+ }
+ }
+ }
+
+ DebugReturn(succ);
+ return succ;
+ }
+
+ ///
+ /// 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.PropertyXml:
+ succ = _state.XmlFragmentNavigator.MoveToNext();
+ break;
+ case StatePosition.Element:
+ succ = false;
+ while (_state.Siblings != null && _state.SiblingIndex < _state.Siblings.Count - 1)
+ {
+ // Siblings may contain IDs that does not correspond to some content in source
+ // because children contains all child IDs including unpublished children - and
+ // then if we're not previewing, the source will return null.
+ var node = _source.Get(_state.Siblings[++_state.SiblingIndex]);
+ if (node == null) continue;
+
+ _state.Content = node;
+ DebugState();
+ succ = true;
+ break;
+ }
+ break;
+ case StatePosition.PropertyElement:
+ if (_state.FieldIndex == _state.FieldsCount - 1)
+ {
+ // after property elements may come some children elements
+ // if successful, will push a new state
+ succ = MoveToFirstChildElement();
+ }
+ else
+ {
+ ++_state.FieldIndex;
+ DebugState();
+ succ = true;
+ }
+ break;
+ case StatePosition.PropertyText:
+ case StatePosition.Attribute:
+ 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.PropertyXml:
+ succ = _state.XmlFragmentNavigator.MoveToPrevious();
+ break;
+ case StatePosition.Element:
+ succ = false;
+ while (_state.Siblings != null && _state.SiblingIndex > 0)
+ {
+ // children may contain IDs that does not correspond to some content in source
+ // because children contains all child IDs including unpublished children - and
+ // then if we're not previewing, the source will return null.
+ var content = _source.Get(_state.Siblings[--_state.SiblingIndex]);
+ if (content == null) continue;
+
+ _state.Content = content;
+ DebugState();
+ succ = true;
+ break;
+ }
+ if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex)
+ {
+ // before children elements may come some property elements
+ if (MoveToParentElement()) // pops the state
+ {
+ _state.FieldIndex = _state.FieldsCount - 1;
+ DebugState();
+ succ = true;
+ }
+ }
+ break;
+ case StatePosition.PropertyElement:
+ succ = false;
+ if (_state.FieldIndex > _lastAttributeIndex)
+ {
+ --_state.FieldIndex;
+ DebugState();
+ succ = true;
+ }
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.PropertyText:
+ 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.PropertyXml:
+ succ = _state.XmlFragmentNavigator.MoveToNextAttribute();
+ break;
+ case StatePosition.Attribute:
+ if (_state.FieldIndex == _lastAttributeIndex)
+ succ = false;
+ else
+ {
+ ++_state.FieldIndex;
+ DebugState();
+ succ = true;
+ }
+ break;
+ case StatePosition.Element:
+ case StatePosition.PropertyElement:
+ case StatePosition.PropertyText:
+ 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.Attribute:
+ case StatePosition.PropertyElement:
+ _state.Position = StatePosition.Element;
+ _state.FieldIndex = -1;
+ DebugState();
+ succ = true;
+ break;
+ case StatePosition.Element:
+ succ = MoveToParentElement();
+ if (!succ)
+ {
+ _state.Position = StatePosition.Root;
+ succ = true;
+ }
+ break;
+ case StatePosition.PropertyText:
+ _state.Position = StatePosition.PropertyElement;
+ DebugState();
+ succ = true;
+ break;
+ case StatePosition.PropertyXml:
+ if (!_state.XmlFragmentNavigator.MoveToParent())
+ throw new InvalidOperationException("Could not move to parent in fragment.");
+ if (_state.XmlFragmentNavigator.NodeType == XPathNodeType.Root)
+ {
+ _state.XmlFragmentNavigator = null;
+ _state.Position = StatePosition.PropertyElement;
+ DebugState();
+ }
+ succ = true;
+ break;
+ case StatePosition.Root:
+ succ = false;
+ break;
+ default:
+ throw new InvalidOperationException("Invalid position.");
+ }
+
+ DebugReturn(succ);
+ return succ;
+ }
+
+ private bool MoveToParentElement()
+ {
+ var p = _state.Parent;
+ if (p != null)
+ {
+ _state = p;
+ DebugState();
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Moves the XPathNavigator to the root node that the current node belongs to.
+ ///
+ public override void MoveToRoot()
+ {
+ DebugEnter("MoveToRoot");
+
+ while (_state.Parent != null)
+ _state = _state.Parent;
+ 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.PropertyXml:
+ type = _state.XmlFragmentNavigator.NodeType;
+ break;
+ case StatePosition.Attribute:
+ type = XPathNodeType.Attribute;
+ break;
+ case StatePosition.Element:
+ case StatePosition.PropertyElement:
+ type = XPathNodeType.Element;
+ break;
+ case StatePosition.PropertyText:
+ 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 content elements, and we start
+ /// reporting values only on property elements. This is because, otherwise, we would dump the whole database
+ /// and it probably does not make sense at Umbraco level.
+ public override string Value
+ {
+ get
+ {
+ DebugEnter("Value");
+ string value;
+
+ switch (_state.Position)
+ {
+ case StatePosition.PropertyXml:
+ value = _state.XmlFragmentNavigator.Value;
+ break;
+ case StatePosition.Attribute:
+ case StatePosition.PropertyText:
+ case StatePosition.PropertyElement:
+ if (_state.FieldIndex == -1)
+ {
+ value = _state.Content.Id.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ var valueForXPath = _state.Content.Value(_state.FieldIndex);
+
+ // value should be
+ // - an XPathNavigator over a non-empty XML fragment
+ // - a non-Xml-whitespace string
+ // - null
+
+ var nav = valueForXPath as XPathNavigator;
+ var s = valueForXPath as string;
+ if (valueForXPath == null)
+ {
+ value = string.Empty;
+ }
+ else if (nav != null)
+ {
+ nav = nav.Clone(); // never use the one we got
+ value = nav.Value;
+ }
+ else if (s != null)
+ {
+ value = s;
+ }
+ else
+ {
+ throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string.");
+ }
+ }
+ break;
+ case StatePosition.Element:
+ case StatePosition.Root:
+ value = string.Empty;
+ break;
+ default:
+ throw new InvalidOperationException("Invalid position.");
+ }
+
+ DebugReturn("\"{0}\"", value);
+ return value;
+ }
+ }
+
+ #region State management
+
+ // the possible state positions
+ internal enum StatePosition
+ {
+ Root,
+ Element,
+ Attribute,
+ PropertyElement,
+ PropertyText,
+ PropertyXml
+ };
+
+ // 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;
+ FieldIndex = -1;
+ }
+
+ // initialize a new state
+ // used for creating the very first state
+ // and also when moving to a child element
+ public State(INavigableContent content, State parent, IList siblings, int siblingIndex, StatePosition position)
+ : this(position)
+ {
+ Content = content;
+ Parent = parent;
+ Siblings = siblings;
+ SiblingIndex = siblingIndex;
+ }
+
+ // initialize a clone state
+ private State(State other, bool recurse = false)
+ {
+ Position = other.Position;
+
+ _content = other._content;
+ SiblingIndex = other.SiblingIndex;
+ Siblings = other.Siblings;
+ FieldsCount = other.FieldsCount;
+ FieldIndex = other.FieldIndex;
+
+ if (Position == StatePosition.PropertyXml)
+ XmlFragmentNavigator = other.XmlFragmentNavigator.Clone();
+
+ // NielsK did
+ //Parent = other.Parent;
+ // but that creates corrupted stacks of states when cloning
+ // because clones share the parents : have to clone the whole
+ // stack of states. Avoid recursion.
+
+ if (recurse) return;
+
+ var clone = this;
+ while (other.Parent != null)
+ {
+ clone.Parent = new State(other.Parent, true);
+ clone = clone.Parent;
+ other = other.Parent;
+ }
+ }
+
+ public State Clone()
+ {
+ return new State(this);
+ }
+
+ // the parent state
+ public State Parent { get; private set; }
+
+ // the current content
+ private INavigableContent _content;
+
+ // the current content
+ public INavigableContent Content
+ {
+ get
+ {
+ return _content;
+ }
+ set
+ {
+ FieldsCount = value == null ? 0 : value.Type.FieldTypes.Length;
+ _content = value;
+ }
+ }
+
+ // the index of the current content within Siblings
+ public int SiblingIndex { get; set; }
+
+ // the list of content identifiers for all children of the current content's parent
+ public IList Siblings { get; set; }
+
+ // the number of fields of the current content
+ // properties include attributes and properties
+ public int FieldsCount { get; private set; }
+
+ // the index of the current field
+ // index -1 means special attribute "id"
+ public int FieldIndex { get; set; }
+
+ // the current field type
+ // beware, no check on the index
+ public INavigableFieldType CurrentFieldType { get { return Content.Type.FieldTypes[FieldIndex]; } }
+
+ // gets or sets the xml fragment navigator
+ public XPathNavigator XmlFragmentNavigator { 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.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator))
+ && other.Content == Content
+ && other.FieldIndex == FieldIndex;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs
new file mode 100644
index 0000000000..c14894bf43
--- /dev/null
+++ b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs
@@ -0,0 +1,1015 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Xml;
+using System.Xml.XPath;
+using System.Xml.Xsl;
+using umbraco;
+using Umbraco.Core;
+using Umbraco.Core.Xml;
+using Umbraco.Core.Xml.XPath;
+using NUnit.Framework;
+
+namespace Umbraco.Tests.CoreXml
+{
+ [TestFixture]
+ public class NavigableNavigatorTests
+ {
+ [Test]
+ public void NewNavigatorIsAtRoot()
+ {
+ const string xml = @" ";
+ var doc = XmlHelper.CreateXPathDocument(xml);
+ var nav = doc.CreateNavigator();
+
+ Assert.AreEqual(XPathNodeType.Root, nav.NodeType);
+ Assert.AreEqual(string.Empty, nav.Name);
+
+ var source = new TestSource5();
+ nav = new NavigableNavigator(source);
+
+ Assert.AreEqual(XPathNodeType.Root, nav.NodeType);
+ Assert.AreEqual(string.Empty, nav.Name);
+ }
+
+ [Test]
+ public void NativeXmlValues()
+ {
+ const string xml = @"
+
+
+
+
+
+
+
+ blah
+
+ blah
+
+
+ bam
+
+
+
+ ";
+ var doc = XmlHelper.CreateXPathDocument(xml);
+ var nav = doc.CreateNavigator();
+
+ NavigatorValues(nav, true);
+ }
+
+ [Test]
+ public void NavigableXmlValues()
+ {
+ var source = new TestSource6();
+ var nav = new NavigableNavigator(source);
+
+ NavigatorValues(nav, false);
+ }
+
+ static void NavigatorValues(XPathNavigator nav, bool native)
+ {
+ // in non-native we can't have Value dump everything, else
+ // we'd dump the entire database? Makes not much sense.
+
+ Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !!
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual("root", nav.Name);
+ Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !!
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual("wrap", nav.Name);
+ Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !!
+
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual("item1", nav.Name);
+ Assert.AreEqual(string.Empty, nav.Value);
+ Assert.IsFalse(nav.MoveToFirstChild());
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item2", nav.Name);
+ Assert.AreEqual(string.Empty, nav.Value);
+ Assert.IsFalse(nav.MoveToFirstChild()); // !!
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item2a", nav.Name);
+ Assert.AreEqual(string.Empty, nav.Value);
+ Assert.IsFalse(nav.MoveToFirstChild()); // !!
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item2b", nav.Name);
+ Assert.AreEqual(string.Empty, nav.Value);
+ Assert.IsFalse(nav.MoveToFirstChild()); // !!
+
+ // we have no way to tell the navigable that a value is CDATA
+ // so the rule is, if it's null it's not there, anything else is there
+ // and the filtering has to be done when building the content
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item2c", nav.Name);
+ Assert.AreEqual("\r\n ", nav.Value); // ok since it's a property
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(XPathNodeType.Text, nav.NodeType);
+ Assert.AreEqual(string.Empty, nav.Name);
+ Assert.AreEqual("\r\n ", nav.Value);
+ Assert.IsTrue(nav.MoveToParent());
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item3", nav.Name);
+ Assert.AreEqual("blah", nav.Value); // ok since it's a property
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(XPathNodeType.Text, nav.NodeType);
+ Assert.AreEqual(string.Empty, nav.Name);
+ Assert.AreEqual("blah", nav.Value);
+ Assert.IsTrue(nav.MoveToParent());
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item3a", nav.Name);
+ Assert.AreEqual("\r\n blah\r\n ", nav.Value); // ok since it's a property
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(XPathNodeType.Text, nav.NodeType);
+ Assert.AreEqual(string.Empty, nav.Name);
+ Assert.AreEqual("\r\n blah\r\n ", nav.Value);
+ Assert.IsTrue(nav.MoveToParent());
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item4", nav.Name);
+ Assert.AreEqual("bam", nav.Value); // ok since it's a property
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual("subitem", nav.Name);
+ Assert.AreEqual("bam", nav.Value); // ok since we're in a fragment
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(XPathNodeType.Text, nav.NodeType);
+ Assert.AreEqual(string.Empty, nav.Name);
+ Assert.AreEqual("bam", nav.Value);
+ Assert.IsFalse(nav.MoveToNext());
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual("subitem", nav.Name);
+ Assert.IsFalse(nav.MoveToNext());
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual("item4", nav.Name);
+
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("item5", nav.Name);
+ Assert.AreEqual("\r\n ", nav.Value);
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(XPathNodeType.Text, nav.NodeType);
+ Assert.AreEqual("\r\n ", nav.Value);
+ }
+
+ [Test]
+ public void Navigate()
+ {
+ var source = new TestSource1();
+ var nav = new NavigableNavigator(source);
+
+ nav.MoveToRoot();
+ Assert.AreEqual("", nav.Name); // because we're at root
+ nav.MoveToFirstChild();
+ Assert.AreEqual("root", nav.Name);
+ nav.MoveToFirstChild();
+ Assert.AreEqual("type1", nav.Name); // our first content
+ nav.MoveToFirstAttribute();
+ Assert.AreEqual("id", nav.Name);
+ Assert.AreEqual("1", nav.Value);
+ nav.MoveToNextAttribute();
+ Assert.AreEqual("prop1", nav.Name);
+ Assert.AreEqual("1:p1", nav.Value);
+ nav.MoveToNextAttribute();
+ Assert.AreEqual("prop2", nav.Name);
+ Assert.AreEqual("1:p2", nav.Value);
+ Assert.IsFalse(nav.MoveToNextAttribute());
+ nav.MoveToParent();
+ nav.MoveToFirstChild();
+ Assert.AreEqual("prop3", nav.Name);
+ Assert.AreEqual("1:p3", nav.Value);
+
+ Assert.IsFalse(nav.MoveToNext());
+ }
+
+ [Test]
+ public void NavigateMixed()
+ {
+ var source = new TestSource2();
+ var nav = new NavigableNavigator(source);
+
+ nav.MoveToRoot();
+ nav.MoveToFirstChild();
+ Assert.AreEqual("root", nav.Name);
+ nav.MoveToFirstChild();
+ Assert.AreEqual("type1", nav.Name); // our content
+ nav.MoveToFirstChild();
+ Assert.AreEqual("prop1", nav.Name); // our property
+ Assert.AreEqual(XPathNodeType.Element, nav.NodeType);
+ nav.MoveToFirstChild();
+
+ // "poo "
+
+ Assert.AreEqual(XPathNodeType.Element, nav.NodeType);
+ Assert.AreEqual("data", nav.Name);
+
+ nav.MoveToFirstChild();
+ Assert.AreEqual(XPathNodeType.Element, nav.NodeType);
+ Assert.AreEqual("item1", nav.Name);
+
+ nav.MoveToNext();
+ Assert.AreEqual(XPathNodeType.Element, nav.NodeType);
+ Assert.AreEqual("item2", nav.Name);
+
+ nav.MoveToParent();
+ Assert.AreEqual(XPathNodeType.Element, nav.NodeType);
+ Assert.AreEqual("data", nav.Name);
+
+ nav.MoveToParent();
+ Assert.AreEqual(XPathNodeType.Element, nav.NodeType);
+ Assert.AreEqual("prop1", nav.Name);
+ }
+
+ [Test]
+ public void OuterXmlBasic()
+ {
+ const string xml = @" ";
+
+ var doc = XmlHelper.CreateXPathDocument(xml);
+ var nnav = doc.CreateNavigator();
+ Assert.AreEqual(xml, nnav.OuterXml);
+
+ var source = new TestSource0();
+ var nav = new NavigableNavigator(source);
+ Assert.AreEqual(xml, nav.OuterXml);
+ }
+
+ [Test]
+ public void OuterXml()
+ {
+ var source = new TestSource1();
+ var nav = new NavigableNavigator(source);
+
+ const string xml = @"
+
+ 1:p3
+
+ ";
+
+ Assert.AreEqual(xml, nav.OuterXml);
+ }
+
+ [Test]
+ public void OuterXmlMixed()
+ {
+ var source = new TestSource2();
+ var nav = new NavigableNavigator(source);
+
+ nav.MoveToRoot();
+
+ const string outerXml = @"
+
+
+
+ poo
+
+
+
+
+
+ ";
+
+ Assert.AreEqual(outerXml, nav.OuterXml);
+ }
+
+ [Test]
+ public void Query()
+ {
+ var source = new TestSource1();
+ var nav = new NavigableNavigator(source);
+
+ var iterator = nav.Select("//type1");
+ Assert.AreEqual(1, iterator.Count);
+ iterator.MoveNext();
+ Assert.AreEqual("type1", iterator.Current.Name);
+
+ iterator = nav.Select("//* [@prop1='1:p1']");
+ Assert.AreEqual(1, iterator.Count);
+ iterator.MoveNext();
+ Assert.AreEqual("type1", iterator.Current.Name);
+ }
+
+ [Test]
+ public void QueryMixed()
+ {
+ var source = new TestSource2();
+ var nav = new NavigableNavigator(source);
+
+ var doc = XmlHelper.CreateXPathDocument("poo ");
+ var docNav = doc.CreateNavigator();
+ var docIter = docNav.Select("//item2 [@xx=33]");
+ Assert.AreEqual(1, docIter.Count);
+ Assert.AreEqual("", docIter.Current.Name);
+ docIter.MoveNext();
+ Assert.AreEqual("item2", docIter.Current.Name);
+
+ var iterator = nav.Select("//item2 [@xx=33]");
+ Assert.AreEqual(1, iterator.Count);
+ Assert.AreEqual("", iterator.Current.Name);
+ iterator.MoveNext();
+ Assert.AreEqual("item2", iterator.Current.Name);
+ }
+
+ [Test]
+ public void QueryWithVariables()
+ {
+ var source = new TestSource1();
+ var nav = new NavigableNavigator(source);
+
+ var iterator = nav.Select("//* [@prop1=$var]", new XPathVariable("var", "1:p1"));
+ Assert.AreEqual(1, iterator.Count);
+ iterator.MoveNext();
+ Assert.AreEqual("type1", iterator.Current.Name);
+ }
+
+ [Test]
+ public void QueryMixedWithVariables()
+ {
+ var source = new TestSource2();
+ var nav = new NavigableNavigator(source);
+
+ var iterator = nav.Select("//item2 [@xx=$var]", new XPathVariable("var", "33"));
+ Assert.AreEqual(1, iterator.Count);
+ iterator.MoveNext();
+ Assert.AreEqual("item2", iterator.Current.Name);
+ }
+
+ [Test]
+ public void MixedWithNoValue()
+ {
+ var source = new TestSource4();
+ var nav = new NavigableNavigator(source);
+
+ var doc = XmlHelper.CreateXPathDocument(@"
+ dang
+
+
+ ");
+ var docNav = doc.CreateNavigator();
+
+ docNav.MoveToRoot();
+ Assert.IsTrue(docNav.MoveToFirstChild());
+ Assert.AreEqual("root", docNav.Name);
+ Assert.IsTrue(docNav.MoveToFirstChild());
+ Assert.AreEqual("type1", docNav.Name);
+ Assert.IsTrue(docNav.MoveToNext());
+ Assert.AreEqual("type1", docNav.Name);
+ Assert.IsTrue(docNav.MoveToNext());
+ Assert.AreEqual("type1", docNav.Name);
+ Assert.IsFalse(docNav.MoveToNext());
+
+ docNav.MoveToRoot();
+ var docOuter = docNav.OuterXml;
+
+ nav.MoveToRoot();
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual("root", nav.Name);
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual("type1", nav.Name);
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("type1", nav.Name);
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual("type1", nav.Name);
+ Assert.IsFalse(nav.MoveToNext());
+
+ nav.MoveToRoot();
+ var outer = nav.OuterXml;
+
+ Assert.AreEqual(docOuter, outer);
+ }
+
+ [Test]
+ [Ignore("NavigableNavigator does not implement IHasXmlNode.")]
+ public void XmlNodeList()
+ {
+ var source = new TestSource1();
+ var nav = new NavigableNavigator(source);
+
+ var iterator = nav.Select("/*");
+
+ // but, that requires that the underlying navigator implements IHasXmlNode
+ // so it is possible to obtain nodes from the navigator - not possible yet
+ var nodes = XmlNodeListFactory.CreateNodeList(iterator);
+
+ Assert.AreEqual(nodes.Count, 1);
+ var node = nodes[0];
+
+ Assert.AreEqual(3, node.Attributes.Count);
+ Assert.AreEqual("1", node.Attributes["id"].Value);
+ Assert.AreEqual("1:p1", node.Attributes["prop1"].Value);
+ Assert.AreEqual("1:p2", node.Attributes["prop2"].Value);
+ Assert.AreEqual(1, node.ChildNodes.Count);
+ Assert.AreEqual("prop3", node.FirstChild.Name);
+ Assert.AreEqual("1:p3", node.FirstChild.Value);
+ }
+
+ [Test]
+ public void CloneIsSafe()
+ {
+ var source = new TestSource5();
+ var nav = new NavigableNavigator(source);
+ TestContent content;
+
+ Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position);
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual("root", nav.Name); // at -1
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id);
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position);
+ Assert.AreEqual("prop1", nav.Name);
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position);
+ Assert.AreEqual("prop2", nav.Name);
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id);
+
+ // at that point nav is at /root/1/3
+
+ var clone = nav.Clone() as NavigableNavigator;
+
+ // move nav to /root/1/5 and ensure that clone stays at /root/1/3
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(5, (nav.UnderlyingObject as TestContent).Id);
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, clone.InternalState.Position);
+ Assert.AreEqual(3, (clone.UnderlyingObject as TestContent).Id);
+
+ // move nav to /root/2
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id);
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(2, (nav.UnderlyingObject as TestContent).Id);
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position);
+ Assert.AreEqual("prop1", nav.Name);
+ Assert.AreEqual("p21", nav.Value);
+
+ // move clone to .. /root/1
+ Assert.IsTrue(clone.MoveToParent());
+
+ // clone has not been corrupted by nav
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, clone.InternalState.Position);
+ Assert.AreEqual(1, (clone.UnderlyingObject as TestContent).Id);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(4)]
+ [TestCase(5)]
+ [TestCase(6)]
+ public void SelectById(int id)
+ {
+ var source = new TestSource5();
+ var nav = new NavigableNavigator(source);
+
+ var iter = nav.Select(string.Format("//* [@id={0}]", id));
+ Assert.IsTrue(iter.MoveNext());
+ var current = iter.Current as NavigableNavigator;
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, current.InternalState.Position);
+ Assert.AreEqual(id, (current.UnderlyingObject as TestContent).Id);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(4)]
+ [TestCase(5)]
+ [TestCase(6)]
+ public void SelectByIdWithVariable(int id)
+ {
+ var source = new TestSource5();
+ var nav = new NavigableNavigator(source);
+
+ var iter = nav.Select("//* [@id=$id]", new XPathVariable("id", id.ToString(CultureInfo.InvariantCulture)));
+ Assert.IsTrue(iter.MoveNext());
+ var current = iter.Current as NavigableNavigator;
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, current.InternalState.Position);
+ Assert.AreEqual(id, (current.UnderlyingObject as TestContent).Id);
+ }
+
+ [Test]
+ public void MoveToId()
+ {
+ var source = new TestSource5();
+ var nav = new NavigableNavigator(source);
+
+ // move to /root/1/3
+ Assert.IsTrue(nav.MoveToId("3"));
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id);
+
+ // move to /root/1
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id);
+
+ // move to /root
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id);
+
+ // move up
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position);
+ Assert.IsFalse(nav.MoveToParent());
+
+ // move to /root/1
+ Assert.IsTrue(nav.MoveToId("1"));
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id);
+
+ // move to /root
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id);
+
+ // move up
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position);
+ Assert.IsFalse(nav.MoveToParent());
+
+ // move to /root
+ Assert.IsTrue(nav.MoveToId("-1"));
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id);
+
+ // move up
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position);
+ Assert.IsFalse(nav.MoveToParent());
+
+ // get lost
+ Assert.IsFalse(nav.MoveToId("666"));
+ }
+
+ [Test]
+ public void RootedNavigator()
+ {
+ var source = new TestSource5();
+ var nav = new NavigableNavigator(source, source.Get(1));
+
+ // go to (/root) /1
+ Assert.IsTrue(nav.MoveToFirstChild());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id);
+
+ // go to (/root) /1/prop1
+ Assert.IsTrue(nav.MoveToFirstChild());
+ // go to (/root) /1/prop2
+ Assert.IsTrue(nav.MoveToNext());
+ // go to (/root) /1/3
+ Assert.IsTrue(nav.MoveToNext());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id);
+
+ // go to (/root) /1
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position);
+ Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id);
+
+ // go to (/root) ie root
+ Assert.IsTrue(nav.MoveToParent());
+ Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position);
+ Assert.IsFalse(nav.MoveToParent());
+
+ // can't go there
+ Assert.IsFalse(nav.MoveToId("2"));
+ }
+
+ [Test]
+ public void WhiteSpacesAndEmptyValues()
+ {
+
+ // "When Microsoft’s DOM builder receives a text node from the parser
+ // that contains only white space, it is thrown away." - so if it's ONLY
+ // spaces, it's nothing, but spaces are NOT trimmed.
+
+ // For attributes, spaces are preserved even when there's only spaces.
+
+ var doc = XmlHelper.CreateXPathDocument(@"
+
+
+
+
+
+ ooo
+ ooo
+
+
+
+ ");
+
+ var docNav = doc.CreateNavigator();
+
+ Assert.AreEqual(@"
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
ooo
+
+ -
+
ooo
+
+
+
+
+ ", docNav.OuterXml);
+
+ docNav.MoveToRoot();
+ Assert.IsTrue(docNav.MoveToFirstChild());
+ Assert.IsTrue(docNav.MoveToFirstChild());
+ Assert.IsTrue(docNav.MoveToFirstChild()); // prop
+ Assert.IsTrue(docNav.IsEmptyElement);
+ Assert.IsTrue(docNav.MoveToParent());
+ Assert.IsTrue(docNav.MoveToNext());
+ Assert.IsTrue(docNav.MoveToFirstChild()); // prop
+ Assert.IsFalse(docNav.IsEmptyElement);
+ Assert.AreEqual("", docNav.Value); // contains an empty text node
+ Assert.IsTrue(docNav.MoveToParent());
+ Assert.IsTrue(docNav.MoveToNext());
+ Assert.IsTrue(docNav.MoveToFirstChild()); // prop
+ Assert.IsFalse(docNav.IsEmptyElement);
+ Assert.AreEqual("", docNav.Value); // contains an empty text node
+
+ var source = new TestSource8();
+ var nav = new NavigableNavigator(source);
+
+ // shows how whitespaces are handled by NavigableNavigator
+ Assert.AreEqual(@"
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ -
+
ooo
+
+ ", nav.OuterXml);
+ }
+ }
+
+ #region Navigable implementation
+
+ class TestPropertyType : INavigableFieldType
+ {
+ public TestPropertyType(string name, bool isXmlContent = false, Func xmlStringConverter = null)
+ {
+ Name = name;
+ IsXmlContent = isXmlContent;
+ XmlStringConverter = xmlStringConverter;
+ }
+
+ public string Name { get; private set; }
+ public bool IsXmlContent { get; private set; }
+ public Func XmlStringConverter { get; private set; }
+ }
+
+ class TestContentType : INavigableContentType
+ {
+ public TestContentType(TestSourceBase source, string name, params INavigableFieldType[] properties)
+ {
+ Source = source;
+ Name = name;
+ FieldTypes = properties;
+ }
+
+ public TestSourceBase Source { get; private set; }
+ public string Name { get; private set; }
+ public INavigableFieldType[] FieldTypes { get; protected set; }
+ }
+
+ class TestRootContentType : TestContentType
+ {
+ public TestRootContentType(TestSourceBase source, params INavigableFieldType[] properties)
+ : base(source, "root")
+ {
+ FieldTypes = properties;
+ }
+
+ public TestContentType CreateType(string name, params INavigableFieldType[] properties)
+ {
+ return new TestContentType(Source, name, FieldTypes.Union(properties).ToArray());
+ }
+ }
+
+ class TestContent : INavigableContent
+ {
+ public TestContent(TestContentType type, int id, int parentId)
+ {
+ _type = type;
+ Id = id;
+ ParentId = parentId;
+ }
+
+ private readonly TestContentType _type;
+ public int Id { get; private set; }
+ public int ParentId { get; private set; }
+ public INavigableContentType Type { get { return _type; } }
+ public IList ChildIds { get; private set; }
+
+ public object Value(int id)
+ {
+ var fieldType = _type.FieldTypes[id];
+ var value = FieldValues[id];
+ var isAttr = id <= _type.Source.LastAttributeIndex;
+
+ if (value == null) return null;
+ if (isAttr) return value.ToString();
+
+ if (fieldType.XmlStringConverter != null) return fieldType.XmlStringConverter(value);
+
+ // though in reality we should use the converters, which should
+ // know whether the property is XML or not, instead of guessing.
+ XPathDocument doc;
+ if (XmlHelper.TryCreateXPathDocumentFromPropertyValue(value, out doc))
+ return doc.CreateNavigator();
+
+ //var s = value.ToString();
+ //return XmlHelper.IsXmlWhitespace(s) ? null : s;
+ return value.ToString();
+ }
+
+ // locals
+ public object[] FieldValues { get; private set; }
+
+ public TestContent WithChildren(params int[] childIds)
+ {
+ ChildIds = childIds;
+ return this;
+ }
+
+ public TestContent WithValues(params object[] values)
+ {
+ FieldValues = values == null ? new object[] {null} : values;
+ return this;
+ }
+ }
+
+ class TestRootContent : TestContent
+ {
+ public TestRootContent(TestContentType type)
+ : base(type, -1, -1)
+ { }
+ }
+
+ abstract class TestSourceBase : INavigableSource
+ {
+ protected readonly Dictionary Content = new Dictionary();
+
+ public INavigableContent Get(int id)
+ {
+ return Content.ContainsKey(id) ? Content[id] : null;
+ }
+
+ public int LastAttributeIndex { get; protected set; }
+
+ public INavigableContent Root { get; protected set; }
+ }
+
+ #endregion
+
+ #region Navigable sources
+
+ class TestSource0 : TestSourceBase
+ {
+ public TestSource0()
+ {
+ LastAttributeIndex = -1;
+ var type = new TestRootContentType(this);
+ Root = new TestRootContent(type);
+ }
+ }
+
+ class TestSource1 : TestSourceBase
+ {
+ public TestSource1()
+ {
+ // last attribute index is 1 - meaning properties 0 and 1 are attributes, 2+ are elements
+ // then, fieldValues must have adequate number of items
+ LastAttributeIndex = 1;
+
+ var prop1 = new TestPropertyType("prop1");
+ var prop2 = new TestPropertyType("prop2");
+ var prop3 = new TestPropertyType("prop3");
+ var type = new TestRootContentType(this, prop1, prop2);
+ var type1 = type.CreateType("type1", prop3);
+
+ Content[1] = new TestContent(type1, 1, -1).WithValues("1:p1", "1:p2", "1:p3");
+
+ Root = new TestRootContent(type).WithValues("", "").WithChildren(1);
+ }
+ }
+
+ class TestSource2 : TestSourceBase
+ {
+ public TestSource2()
+ {
+ LastAttributeIndex = -1;
+
+ var prop1 = new TestPropertyType("prop1", true);
+ var type = new TestRootContentType(this);
+ var type1 = type.CreateType("type1", prop1);
+
+ const string xml = "poo ";
+ Content[1] = new TestContent(type1, 1, 1).WithValues(xml);
+
+ Root = new TestRootContent(type).WithChildren(1);
+ }
+ }
+
+ class TestSource3 : TestSourceBase
+ {
+ public TestSource3()
+ {
+ LastAttributeIndex = 1;
+
+ var prop1 = new TestPropertyType("prop1");
+ var prop2 = new TestPropertyType("prop2");
+ var prop3 = new TestPropertyType("prop3");
+ var type = new TestRootContentType(this, prop1, prop2);
+ var type1 = type.CreateType("type1", prop3);
+
+ Content[1] = new TestContent(type1, 1, 1).WithValues("1:p1", "1:p2", "1:p3").WithChildren(2);
+ Content[2] = new TestContent(type1, 2, 1).WithValues("2:p1", "2:p2", "2:p3");
+
+ Root = new TestRootContent(type).WithChildren(1);
+ }
+ }
+
+ class TestSource4 : TestSourceBase
+ {
+ public TestSource4()
+ {
+ LastAttributeIndex = -1;
+
+ var prop1 = new TestPropertyType("prop1", true);
+ var prop2 = new TestPropertyType("prop2");
+ var type = new TestRootContentType(this);
+ var type1 = type.CreateType("type1", prop1, prop2);
+
+ Content[1] = new TestContent(type1, 1, -1).WithValues("", "dang");
+ Content[2] = new TestContent(type1, 2, -1).WithValues(null, "");
+ Content[3] = new TestContent(type1, 3, -1).WithValues(null, null);
+
+ Root = new TestRootContent(type).WithChildren(1, 2, 3);
+ }
+ }
+
+ class TestSource5 : TestSourceBase
+ {
+ public TestSource5()
+ {
+ LastAttributeIndex = -1;
+
+ var prop1 = new TestPropertyType("prop1");
+ var prop2 = new TestPropertyType("prop2");
+ var type = new TestRootContentType(this);
+ var type1 = type.CreateType("type1", prop1, prop2);
+
+ Content[1] = new TestContent(type1, 1, -1).WithValues("p11", "p12").WithChildren(3, 5);
+ Content[2] = new TestContent(type1, 2, -1).WithValues("p21", "p22").WithChildren(4, 6);
+ Content[3] = new TestContent(type1, 3, 1).WithValues("p31", "p32");
+ Content[4] = new TestContent(type1, 4, 2).WithValues("p41", "p42");
+ Content[5] = new TestContent(type1, 5, 1).WithValues("p51", "p52");
+ Content[6] = new TestContent(type1, 6, 2).WithValues("p61", "p62");
+
+ Root = new TestRootContent(type).WithChildren(1, 2);
+ }
+ }
+
+ class TestSource6 : TestSourceBase
+ {
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // blah
+ //
+ // bam
+ //
+ //
+ //
+ //
+ //
+
+ public TestSource6()
+ {
+ LastAttributeIndex = -1;
+
+ var type = new TestRootContentType(this);
+ var type1 = type.CreateType("wrap",
+ new TestPropertyType("item1"),
+ new TestPropertyType("item2"),
+ new TestPropertyType("item2a"),
+ new TestPropertyType("item2b"),
+ new TestPropertyType("item2c"),
+ new TestPropertyType("item3"),
+ new TestPropertyType("item3a"),
+ new TestPropertyType("item4", true),
+ new TestPropertyType("item5", true)
+ );
+
+ Content[1] = new TestContent(type1, 1, -1)
+ .WithValues(
+ null,
+ null,
+ null,
+ null,
+ "\r\n ",
+ "blah",
+ "\r\n blah\r\n ",
+ "bam ",
+ "\r\n "
+ );
+
+ Root = new TestRootContent(type).WithChildren(1);
+ }
+ }
+
+ class TestSource7 : TestSourceBase
+ {
+ public TestSource7()
+ {
+ LastAttributeIndex = 0;
+
+ var prop1 = new TestPropertyType("isDoc");
+ var prop2 = new TestPropertyType("title");
+ var type = new TestRootContentType(this, prop1);
+ var type1 = type.CreateType("node", prop1, prop2);
+
+ Content[1] = new TestContent(type1, 1, -1).WithValues(1, "title-1").WithChildren(3, 5);
+ Content[2] = new TestContent(type1, 2, -1).WithValues(1, "title-2").WithChildren(4, 6);
+ Content[3] = new TestContent(type1, 3, 1).WithValues(1, "title-3").WithChildren(7, 8);
+ Content[4] = new TestContent(type1, 4, 2).WithValues(1, "title-4");
+ Content[5] = new TestContent(type1, 5, 1).WithValues(1, "title-5");
+ Content[6] = new TestContent(type1, 6, 2).WithValues(1, "title-6");
+
+ Content[7] = new TestContent(type1, 7, 3).WithValues(1, "title-7");
+ Content[8] = new TestContent(type1, 8, 3).WithValues(1, "title-8");
+
+ Root = new TestRootContent(type).WithValues(null).WithChildren(1, 2);
+ }
+ }
+
+ class TestSource8 : TestSourceBase
+ {
+ public TestSource8()
+ {
+ LastAttributeIndex = 0;
+
+ var attr = new TestPropertyType("attr");
+ var prop = new TestPropertyType("prop");
+ var type = new TestRootContentType(this, attr);
+ var type1 = type.CreateType("item", attr, prop);
+ Content[1] = new TestContent(type1, 1, -1).WithValues(null, null);
+ Content[2] = new TestContent(type1, 2, -1).WithValues("", "");
+ Content[3] = new TestContent(type1, 3, -1).WithValues(" ", " ");
+ Content[4] = new TestContent(type1, 4, -1).WithValues("", "\r\n");
+ Content[5] = new TestContent(type1, 5, -1).WithValues(" ooo ", " ooo ");
+ Root = new TestRootContent(type).WithValues(null).WithChildren(1, 2, 3, 4, 5);
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index e8547ec121..e6c3e0f03e 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -206,6 +206,7 @@
+