#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(); } } }