#region Disclaimer / License
// Copyright (C) 2013, Jackie Ng
// http://trac.osgeo.org/mapguide/wiki/maestro, jumpinjackie@gmail.com
//
// Original code from SharpDevelop 3.2.1 licensed under the same terms (LGPL 2.1)
// Copyright 2002-2010 by
//
// AlphaSierraPapa, Christoph Wille
// Vordernberger Strasse 27/8
// A-8700 Leoben
// Austria
//
// email: office@alphasierrapapa.com
// court of jurisdiction: Landesgericht Leoben
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion Disclaimer / License
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace Maestro.Editors.Generic.XmlEditor.AutoCompletion
{
///
/// Utility class that contains xml parsing routines used to determine
/// the currently selected element so we can provide intellisense.
///
///
/// All of the routines return objects
/// since we are interested in the complete path or tree to the
/// currently active element.
///
internal class XmlParser
{
///
/// Helper class. Holds the namespace URI and the prefix currently
/// in use for this namespace.
///
private class NamespaceURI
{
private string namespaceURI = String.Empty;
private string prefix = String.Empty;
public NamespaceURI()
{
}
public NamespaceURI(string namespaceURI, string prefix)
{
this.namespaceURI = namespaceURI;
this.prefix = prefix;
}
public string Namespace
{
get { return namespaceURI; }
set { namespaceURI = value; }
}
public string Prefix
{
get { return prefix; }
set
{
prefix = value;
if (prefix == null)
{
prefix = String.Empty;
}
}
}
public override string ToString()
{
if (!String.IsNullOrEmpty(prefix))
{
return prefix + ":" + namespaceURI;
}
return namespaceURI;
}
}
private static readonly char[] whitespaceCharacters = { ' ', '\n', '\t', '\r' };
private XmlParser()
{
}
///
/// Gets path of the xml element start tag that the specified
/// is currently inside.
///
/// If the index outside the start tag then an empty path
/// is returned.
public static XmlElementPath GetActiveElementStartPath(string xml, int index)
{
QualifiedNameCollection namespaces = new QualifiedNameCollection();
return GetActiveElementStartPath(xml, index, namespaces);
}
///
/// Gets path of the xml element start tag that the specified
/// is currently located. This is different to the
/// GetActiveElementStartPath method since the index can be inside the element
/// name.
///
/// If the index outside the start tag then an empty path
/// is returned.
public static XmlElementPath GetActiveElementStartPathAtIndex(string xml, int index)
{
QualifiedNameCollection namespaces = new QualifiedNameCollection();
return GetActiveElementStartPathAtIndex(xml, index, namespaces);
}
///
/// Gets the parent element path based on the index position.
///
public static XmlElementPath GetParentElementPath(string xml)
{
QualifiedNameCollection namespaces = new QualifiedNameCollection();
XmlElementPath path = GetFullParentElementPath(xml, namespaces);
path.Compact();
return path;
}
///
/// Checks whether the attribute at the end of the string is a
/// namespace declaration.
///
public static bool IsNamespaceDeclaration(string xml, int index)
{
if (String.IsNullOrEmpty(xml))
{
return false;
}
index = GetCorrectedIndex(xml.Length, index);
// Move back one character if the last character is an '='
if (xml[index] == '=')
{
xml = xml.Substring(0, xml.Length - 1);
--index;
}
// From the end of the string work backwards until we have
// picked out the last attribute and reached some whitespace.
StringBuilder reversedAttributeName = new StringBuilder();
bool ignoreWhitespace = true;
int currentIndex = index;
for (int i = 0; i < index; ++i)
{
char currentChar = xml[currentIndex];
if (Char.IsWhiteSpace(currentChar))
{
if (ignoreWhitespace == false)
{
// Reached the start of the attribute name.
break;
}
}
else if (Char.IsLetterOrDigit(currentChar) || (currentChar == ':'))
{
ignoreWhitespace = false;
reversedAttributeName.Append(currentChar);
}
else
{
// Invalid string.
break;
}
--currentIndex;
}
// Did we get a namespace?
bool isNamespace = false;
if ((reversedAttributeName.ToString() == "snlmx") || (reversedAttributeName.ToString().EndsWith(":snlmx")))
{
isNamespace = true;
}
return isNamespace;
}
///
/// Gets the attribute name and any prefix. The namespace
/// is not determined.
///
/// if no attribute name can
/// be found.
public static QualifiedName GetQualifiedAttributeName(string xml, int index)
{
string name = GetAttributeName(xml, index);
return GetQualifiedName(name);
}
///
/// Gets the name of the attribute inside but before the specified
/// index.
///
public static string GetAttributeName(string xml, int index)
{
if (String.IsNullOrEmpty(xml))
{
return String.Empty;
}
index = GetCorrectedIndex(xml.Length, index);
return GetAttributeName(xml, index, true, true, true);
}
///
/// Gets the name of the attribute and its prefix at the specified index. The index
/// can be anywhere inside the attribute name or in the attribute value.
/// The namespace for the element containing the attribute will also be determined
/// if the includeNamespace flag is set to true.
///
public static QualifiedName GetQualifiedAttributeNameAtIndex(string xml, int index, bool includeNamespace)
{
string name = GetAttributeNameAtIndex(xml, index);
QualifiedName qualifiedName = GetQualifiedName(name);
if (qualifiedName != null && String.IsNullOrEmpty(qualifiedName.Namespace) && includeNamespace)
{
QualifiedNameCollection namespaces = new QualifiedNameCollection();
XmlElementPath path = GetActiveElementStartPathAtIndex(xml, index, namespaces);
qualifiedName.Namespace = GetNamespaceForPrefix(namespaces, path.Elements.LastPrefix);
}
return qualifiedName;
}
///
/// Gets the name of the attribute and its prefix at the specified index. The index
/// can be anywhere inside the attribute name or in the attribute value.
///
public static QualifiedName GetQualifiedAttributeNameAtIndex(string xml, int index)
{
return GetQualifiedAttributeNameAtIndex(xml, index, false);
}
///
/// Gets the name of the attribute at the specified index. The index
/// can be anywhere inside the attribute name or in the attribute value.
///
public static string GetAttributeNameAtIndex(string xml, int index)
{
if (String.IsNullOrEmpty(xml))
{
return String.Empty;
}
index = GetCorrectedIndex(xml.Length, index);
bool ignoreWhitespace = true;
bool ignoreEqualsSign = false;
bool ignoreQuote = false;
if (IsInsideAttributeValue(xml, index))
{
// Find attribute name start.
int elementStartIndex = GetActiveElementStartIndex(xml, index);
if (elementStartIndex == -1)
{
return String.Empty;
}
// Find equals sign.
for (int i = index; i > elementStartIndex; --i)
{
char ch = xml[i];
if (ch == '=')
{
index = i;
ignoreEqualsSign = true;
break;
}
}
}
else
{
// Find end of attribute name.
for (; index < xml.Length; ++index)
{
char ch = xml[index];
if (!IsXmlNameChar(ch))
{
if (ch == '\'' || ch == '\"')
{
ignoreQuote = true;
ignoreEqualsSign = true;
}
break;
}
}
--index;
}
return GetAttributeName(xml, index, ignoreWhitespace, ignoreQuote, ignoreEqualsSign);
}
///
/// Checks for valid xml attribute value character
///
public static bool IsAttributeValueChar(char ch)
{
if ((ch == '<') ||
(ch == '>'))
{
return false;
}
return true;
}
///
/// Checks for valid xml element or attribute name character.
///
public static bool IsXmlNameChar(char ch)
{
if (Char.IsLetterOrDigit(ch) ||
(ch == ':') ||
(ch == '/') ||
(ch == '_') ||
(ch == '.') ||
(ch == '-'))
{
return true;
}
return false;
}
///
/// Determines whether the specified index is inside an attribute value.
///
public static bool IsInsideAttributeValue(string xml, int index)
{
if (String.IsNullOrEmpty(xml))
{
return false;
}
if (index > xml.Length)
{
index = xml.Length;
}
int elementStartIndex = GetActiveElementStartIndex(xml, index);
if (elementStartIndex == -1)
{
return false;
}
// Count the number of double quotes and single quotes that exist
// before the first equals sign encountered going backwards to
// the start of the active element.
bool foundEqualsSign = false;
int doubleQuotesCount = 0;
int singleQuotesCount = 0;
char lastQuoteChar = ' ';
for (int i = index - 1; i > elementStartIndex; --i)
{
char ch = xml[i];
if (ch == '=')
{
foundEqualsSign = true;
break;
}
else if (ch == '\"')
{
lastQuoteChar = ch;
++doubleQuotesCount;
}
else if (ch == '\'')
{
lastQuoteChar = ch;
++singleQuotesCount;
}
}
bool isInside = false;
if (foundEqualsSign)
{
// Odd number of quotes?
if ((lastQuoteChar == '\"') && ((doubleQuotesCount % 2) > 0))
{
isInside = true;
}
else if ((lastQuoteChar == '\'') && ((singleQuotesCount % 2) > 0))
{
isInside = true;
}
}
return isInside;
}
///
/// Gets the attribute value at the specified index.
///
/// An empty string if no attribute value can be found.
public static string GetAttributeValueAtIndex(string xml, int index)
{
if (!IsInsideAttributeValue(xml, index))
{
return String.Empty;
}
index = GetCorrectedIndex(xml.Length, index);
int elementStartIndex = GetActiveElementStartIndex(xml, index);
if (elementStartIndex == -1)
{
return String.Empty;
}
// Find equals sign.
int equalsSignIndex = -1;
for (int i = index; i > elementStartIndex; --i)
{
char ch = xml[i];
if (ch == '=')
{
equalsSignIndex = i;
break;
}
}
if (equalsSignIndex == -1)
{
return String.Empty;
}
// Find attribute value.
char quoteChar = ' ';
bool foundQuoteChar = false;
StringBuilder attributeValue = new StringBuilder();
for (int i = equalsSignIndex; i < xml.Length; ++i)
{
char ch = xml[i];
if (!foundQuoteChar)
{
if (ch == '\"' || ch == '\'')
{
quoteChar = ch;
foundQuoteChar = true;
}
}
else
{
if (ch == quoteChar)
{
// End of attribute value.
return attributeValue.ToString();
}
else if (IsAttributeValueChar(ch) || (ch == '\"' || ch == '\''))
{
attributeValue.Append(ch);
}
else
{
// Invalid character found.
return String.Empty;
}
}
}
return String.Empty;
}
///
/// Gets the text of the xml element start tag that the index is
/// currently inside.
///
///
/// Returns the text up to and including the start tag < character.
///
private static string GetActiveElementStartText(string xml, int index)
{
int elementStartIndex = GetActiveElementStartIndex(xml, index);
if (elementStartIndex >= 0)
{
if (elementStartIndex < index)
{
int elementEndIndex = GetActiveElementEndIndex(xml, index);
if (elementEndIndex >= index)
{
return xml.Substring(elementStartIndex, elementEndIndex - elementStartIndex);
}
}
}
return null;
}
///
/// Locates the index of the start tag < character.
///
///
/// Returns the index of the start tag character; otherwise
/// -1 if no start tag character is found or a end tag
/// > character is found first.
///
private static int GetActiveElementStartIndex(string xml, int index)
{
int elementStartIndex = -1;
int currentIndex = index - 1;
for (int i = 0; i < index; ++i)
{
char currentChar = xml[currentIndex];
if (currentChar == '<')
{
elementStartIndex = currentIndex;
break;
}
else if (currentChar == '>')
{
break;
}
--currentIndex;
}
return elementStartIndex;
}
///
/// Locates the index of the end tag character.
///
///
/// Returns the index of the end tag character; otherwise
/// -1 if no end tag character is found or a start tag
/// character is found first.
///
private static int GetActiveElementEndIndex(string xml, int index)
{
int elementEndIndex = index;
for (int i = index; i < xml.Length; ++i)
{
char currentChar = xml[i];
if (currentChar == '>')
{
elementEndIndex = i;
break;
}
else if (currentChar == '<')
{
elementEndIndex = -1;
break;
}
}
return elementEndIndex;
}
///
/// Gets the element name from the element start tag string.
///
/// This string must start at the
/// element we are interested in.
private static QualifiedName GetElementName(string xml)
{
string name = String.Empty;
// Find the end of the element name.
xml = xml.Replace("\r\n", " ");
int index = xml.IndexOf(' ');
if (index > 0)
{
name = xml.Substring(1, index - 1);
}
else
{
name = xml.Substring(1);
}
return GetQualifiedName(name);
}
///
/// Gets the element namespace from the element start tag
/// string.
///
/// This string must start at the
/// element we are interested in.
private static NamespaceURI GetElementNamespace(string xml)
{
NamespaceURI namespaceURI = new NamespaceURI();
Match match = Regex.Match(xml, ".*?(xmlns\\s*?|xmlns:.*?)=\\s*?['\\\"](.*?)['\\\"]");
if (match.Success)
{
namespaceURI.Namespace = match.Groups[2].Value;
string xmlns = match.Groups[1].Value.Trim();
int prefixIndex = xmlns.IndexOf(':');
if (prefixIndex > 0)
{
namespaceURI.Prefix = xmlns.Substring(prefixIndex + 1);
}
}
return namespaceURI;
}
private static string ReverseString(string text)
{
StringBuilder reversedString = new StringBuilder(text);
int index = text.Length;
foreach (char ch in text)
{
--index;
reversedString[index] = ch;
}
return reversedString.ToString();
}
///
/// Ensures that the index is on the last character if it is
/// too large.
///
/// The length of the string.
/// The current index.
/// The index unchanged if the index is smaller than the
/// length of the string; otherwise it returns length - 1.
private static int GetCorrectedIndex(int length, int index)
{
if (index >= length)
{
index = length - 1;
}
return index;
}
///
/// Gets the active element path given the element text.
///
private static XmlElementPath GetActiveElementStartPath(string xml, int index, string elementText, QualifiedNameCollection namespaces)
{
QualifiedName elementName = GetElementName(elementText);
if (elementName == null)
{
return new XmlElementPath();
}
NamespaceURI elementNamespace = GetElementNamespace(elementText);
XmlElementPath path = GetFullParentElementPath(xml.Substring(0, index), namespaces);
// Try to get a namespace for the active element's prefix.
if (elementName.Prefix.Length > 0 && elementNamespace.Namespace.Length == 0)
{
elementName.Namespace = GetNamespaceForPrefix(namespaces, elementName.Prefix);
elementNamespace.Namespace = elementName.Namespace;
elementNamespace.Prefix = elementName.Prefix;
}
if (elementNamespace.Namespace.Length == 0)
{
if (path.Elements.Count > 0)
{
QualifiedName parentName = path.Elements[path.Elements.Count - 1];
elementNamespace.Namespace = parentName.Namespace;
elementNamespace.Prefix = parentName.Prefix;
}
}
path.Elements.Add(new QualifiedName(elementName.Name, elementNamespace.Namespace, elementNamespace.Prefix));
path.Compact();
return path;
}
private static string GetAttributeName(string xml, int index, bool ignoreWhitespace, bool ignoreQuote, bool ignoreEqualsSign)
{
string name = String.Empty;
// From the end of the string work backwards until we have
// picked out the attribute name.
StringBuilder reversedAttributeName = new StringBuilder();
int currentIndex = index;
bool invalidString = true;
for (int i = 0; i <= index; ++i)
{
char currentChar = xml[currentIndex];
if (IsXmlNameChar(currentChar))
{
if (!ignoreEqualsSign)
{
ignoreWhitespace = false;
reversedAttributeName.Append(currentChar);
}
}
else if (Char.IsWhiteSpace(currentChar))
{
if (ignoreWhitespace == false)
{
// Reached the start of the attribute name.
invalidString = false;
break;
}
}
else if ((currentChar == '\'') || (currentChar == '\"'))
{
if (ignoreQuote)
{
ignoreQuote = false;
}
else
{
break;
}
}
else if (currentChar == '=')
{
if (ignoreEqualsSign)
{
ignoreEqualsSign = false;
}
else
{
break;
}
}
else if (IsAttributeValueChar(currentChar))
{
if (!ignoreQuote)
{
break;
}
}
else
{
break;
}
--currentIndex;
}
if (!invalidString)
{
name = ReverseString(reversedAttributeName.ToString());
}
return name;
}
///
/// Gets the element name at the specified index.
///
private static string GetElementNameAtIndex(string xml, int index)
{
int elementStartIndex = GetActiveElementStartIndex(xml, index);
if (elementStartIndex >= 0 && elementStartIndex < index)
{
int elementEndIndex = GetActiveElementEndIndex(xml, index);
if (elementEndIndex == -1)
{
elementEndIndex = xml.IndexOfAny(whitespaceCharacters, elementStartIndex);
}
if (elementEndIndex >= elementStartIndex)
{
return xml.Substring(elementStartIndex, elementEndIndex - elementStartIndex);
}
}
return null;
}
///
/// Returns a name and its prefix.
///
private static QualifiedName GetQualifiedName(string name)
{
if (name.Length == 0)
{
return null;
}
QualifiedName qualifiedName = new QualifiedName();
int prefixIndex = name.IndexOf(':');
if (prefixIndex > 0)
{
qualifiedName.Prefix = name.Substring(0, prefixIndex);
qualifiedName.Name = name.Substring(prefixIndex + 1);
}
else
{
qualifiedName.Name = name;
}
return qualifiedName;
}
///
/// Gets the parent element path based on the index position. This
/// method does not compact the path so it will include all elements
/// including those in another namespace in the path.
///
private static XmlElementPath GetFullParentElementPath(string xml, QualifiedNameCollection namespaces)
{
XmlElementPath path = new XmlElementPath();
IDictionary namespacesInScope = null;
using (StringReader reader = new StringReader(xml))
{
using (XmlTextReader xmlReader = new XmlTextReader(reader))
{
try
{
xmlReader.XmlResolver = null; // prevent XmlTextReader from loading external DTDs
while (xmlReader.Read())
{
switch (xmlReader.NodeType)
{
case XmlNodeType.Element:
if (!xmlReader.IsEmptyElement)
{
QualifiedName elementName = new QualifiedName(xmlReader.LocalName, xmlReader.NamespaceURI, xmlReader.Prefix);
path.Elements.Add(elementName);
}
break;
case XmlNodeType.EndElement:
path.Elements.RemoveLast();
break;
}
}
}
catch (XmlException)
{
namespacesInScope = xmlReader.GetNamespacesInScope(XmlNamespaceScope.All);
}
}
}
// Add namespaces in scope for the last element read.
if (namespacesInScope != null)
{
foreach (KeyValuePair ns in namespacesInScope)
{
namespaces.Add(new QualifiedName(String.Empty, ns.Value, ns.Key));
}
}
return path;
}
///
/// Finds the namespace for the specified prefix.
///
private static string GetNamespaceForPrefix(QualifiedNameCollection namespaces, string prefix)
{
foreach (QualifiedName name in namespaces)
{
if (name.Prefix == prefix)
{
return name.Namespace;
}
}
return String.Empty;
}
///
/// Gets path of the xml element start tag that the specified
/// is currently inside.
///
/// If the index outside the start tag then an empty path
/// is returned.
///
///
/// Returns the namespaces that are
/// exist in the xml.
private static XmlElementPath GetActiveElementStartPath(string xml, int index, QualifiedNameCollection namespaces)
{
XmlElementPath path = new XmlElementPath();
string elementText = GetActiveElementStartText(xml, index);
if (elementText != null)
{
path = GetActiveElementStartPath(xml, index, elementText, namespaces);
}
return path;
}
///
/// Gets path of the xml element start tag that the specified
/// is currently located. This is different to the
/// GetActiveElementStartPath method since the index can be inside the element
/// name.
///
/// If the index outside the start tag then an empty path
/// is returned.
private static XmlElementPath GetActiveElementStartPathAtIndex(string xml, int index, QualifiedNameCollection namespaces)
{
// Find first non xml element name character to the right of the index.
index = GetCorrectedIndex(xml.Length, index);
if (index < 0) // can happen when xml.Length==0
return new XmlElementPath();
int currentIndex = index;
for (; currentIndex < xml.Length; ++currentIndex)
{
char ch = xml[currentIndex];
if (!IsXmlNameChar(ch))
{
break;
}
}
string elementText = GetElementNameAtIndex(xml, currentIndex);
if (elementText != null)
{
return GetActiveElementStartPath(xml, currentIndex, elementText, namespaces);
}
return new XmlElementPath();
}
}
}