diff options
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml')
25 files changed, 3823 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttribute.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttribute.cs new file mode 100644 index 000000000..b8b726ef9 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttribute.cs @@ -0,0 +1,129 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Name-value pair in a tag + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public class AXmlAttribute: AXmlObject + { + /// <summary> Name with namespace prefix - exactly as in source file </summary> + public string Name { get; internal set; } + /// <summary> Equals sign and surrounding whitespace </summary> + public string EqualsSign { get; internal set; } + /// <summary> The raw value - exactly as in source file (*probably* quoted and escaped) </summary> + public string QuotedValue { get; internal set; } + /// <summary> Unquoted and dereferenced value of the attribute </summary> + public string Value { get; internal set; } + + internal override void DebugCheckConsistency(bool checkParentPointers) + { + DebugAssert(Name != null, "Null Name"); + DebugAssert(EqualsSign != null, "Null EqualsSign"); + DebugAssert(QuotedValue != null, "Null QuotedValue"); + DebugAssert(Value != null, "Null Value"); + base.DebugCheckConsistency(checkParentPointers); + } + + #region Helpper methods + + /// <summary> The element containing this attribute </summary> + /// <returns> Null if orphaned </returns> + public AXmlElement ParentElement { + get { + AXmlTag tag = this.Parent as AXmlTag; + if (tag != null) { + return tag.Parent as AXmlElement; + } + return null; + } + } + + /// <summary> The part of name before ":"</summary> + /// <returns> Empty string if not found </returns> + public string Prefix { + get { + return GetNamespacePrefix(this.Name); + } + } + + /// <summary> The part of name after ":" </summary> + /// <returns> Whole name if ":" not found </returns> + public string LocalName { + get { + return GetLocalName(this.Name); + } + } + + /// <summary> + /// Resolved namespace of the name. Empty string if not found + /// From the specification: "The namespace name for an unprefixed attribute name always has no value." + /// </summary> + public string Namespace { + get { + if (string.IsNullOrEmpty(this.Prefix)) return NoNamespace; + + AXmlElement elem = this.ParentElement; + if (elem != null) { + return elem.ResolvePrefix(this.Prefix); + } + return NoNamespace; // Orphaned attribute + } + } + + /// <summary> Attribute is declaring namespace ("xmlns" or "xmlns:*") </summary> + public bool IsNamespaceDeclaration { + get { + return this.Name == "xmlns" || this.Prefix == "xmlns"; + } + } + + #endregion + + /// <inheritdoc/> + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitAttribute(this); + } + + /// <inheritdoc/> + internal override bool UpdateDataFrom(AXmlObject source) + { + if (!base.UpdateDataFrom(source)) return false; + AXmlAttribute src = (AXmlAttribute)source; + if (this.Name != src.Name || + this.EqualsSign != src.EqualsSign || + this.QuotedValue != src.QuotedValue || + this.Value != src.Value) + { + OnChanging(); + this.Name = src.Name; + this.EqualsSign = src.EqualsSign; + this.QuotedValue = src.QuotedValue; + this.Value = src.Value; + OnChanged(); + return true; + } else { + return false; + } + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} '{1}{2}{3}']", base.ToString(), this.Name, this.EqualsSign, this.Value); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttributeCollection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttributeCollection.cs new file mode 100644 index 000000000..95fa83cd0 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttributeCollection.cs @@ -0,0 +1,119 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Specailized attribute collection with attribute name caching + /// </summary> + public class AXmlAttributeCollection: FilteredCollection<AXmlAttribute, AXmlObjectCollection<AXmlObject>> + { + /// <summary> Empty unbound collection </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "InsertItem prevents modifying the Empty collection")] + public static readonly AXmlAttributeCollection Empty = new AXmlAttributeCollection(); + + /// <summary> Create unbound collection </summary> + protected AXmlAttributeCollection() {} + + /// <summary> Wrap the given collection. Non-attributes are filtered </summary> + public AXmlAttributeCollection(AXmlObjectCollection<AXmlObject> source): base(source) {} + + /// <summary> Wrap the given collection. Non-attributes are filtered. Items not matching the condition are filtered. </summary> + public AXmlAttributeCollection(AXmlObjectCollection<AXmlObject> source, Predicate<object> condition): base(source, condition) {} + + Dictionary<string, List<AXmlAttribute>> hashtable = new Dictionary<string, List<AXmlAttribute>>(); + + void AddToHashtable(AXmlAttribute attr) + { + string localName = attr.LocalName; + if (!hashtable.ContainsKey(localName)) { + hashtable[localName] = new List<AXmlAttribute>(1); + } + hashtable[localName].Add(attr); + } + + void RemoveFromHashtable(AXmlAttribute attr) + { + string localName = attr.LocalName; + hashtable[localName].Remove(attr); + } + + static List<AXmlAttribute> NoAttributes = new List<AXmlAttribute>(); + + /// <summary> + /// Get all attributes with given local name. + /// Hash table is used for lookup so this is cheap. + /// </summary> + public IEnumerable<AXmlAttribute> GetByLocalName(string localName) + { + if (hashtable.ContainsKey(localName)) { + return hashtable[localName]; + } else { + return NoAttributes; + } + } + + /// <inheritdoc/> + protected override void ClearItems() + { + foreach(AXmlAttribute item in this) { + RemoveFromHashtable(item); + item.Changing -= item_Changing; + item.Changed -= item_Changed; + } + base.ClearItems(); + } + + /// <inheritdoc/> + protected override void InsertItem(int index, AXmlAttribute item) + { + // prevent insertions into the static 'Empty' instance + if (this == Empty) + throw new NotSupportedException("Cannot insert into AXmlAttributeCollection.Empty"); + + AddToHashtable(item); + item.Changing += item_Changing; + item.Changed += item_Changed; + base.InsertItem(index, item); + } + + /// <inheritdoc/> + protected override void RemoveItem(int index) + { + RemoveFromHashtable(this[index]); + this[index].Changing -= item_Changing; + this[index].Changed -= item_Changed; + base.RemoveItem(index); + } + + /// <inheritdoc/> + protected override void SetItem(int index, AXmlAttribute item) + { + RemoveFromHashtable(this[index]); + this[index].Changing -= item_Changing; + this[index].Changed -= item_Changed; + + AddToHashtable(item); + item.Changing += item_Changing; + item.Changed += item_Changed; + base.SetItem(index, item); + } + + // Every item in the collection should be registered to these handlers + // so that we can handle renames + + void item_Changing(object sender, AXmlObjectEventArgs e) + { + RemoveFromHashtable((AXmlAttribute)e.Object); + } + + void item_Changed(object sender, AXmlObjectEventArgs e) + { + AddToHashtable((AXmlAttribute)e.Object); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlContainer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlContainer.cs new file mode 100644 index 000000000..3cc716de5 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlContainer.cs @@ -0,0 +1,282 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Abstact base class for all types that can contain child nodes + /// </summary> + public abstract class AXmlContainer: AXmlObject + { + /// <summary> + /// Children of the node. It is read-only. + /// Note that is has CollectionChanged event. + /// </summary> + public AXmlObjectCollection<AXmlObject> Children { get; private set; } + + /// <summary> Create new container </summary> + protected AXmlContainer() + { + this.Children = new AXmlObjectCollection<AXmlObject>(); + } + + #region Helpper methods + + ObservableCollection<AXmlElement> elements; + + /// <summary> Gets direcly nested elements (non-recursive) </summary> + public ObservableCollection<AXmlElement> Elements { + get { + if (elements == null) { + elements = new FilteredCollection<AXmlElement, AXmlObjectCollection<AXmlObject>>(this.Children); + } + return elements; + } + } + + internal AXmlObject FirstChild { + get { + return this.Children[0]; + } + } + + internal AXmlObject LastChild { + get { + return this.Children[this.Children.Count - 1]; + } + } + + #endregion + + /// <inheritdoc/> + public override IEnumerable<AXmlObject> GetSelfAndAllChildren() + { + return (new AXmlObject[] { this }).Flatten( + delegate(AXmlObject i) { + AXmlContainer container = i as AXmlContainer; + if (container != null) + return container.Children; + else + return null; + } + ); + } + + /// <summary> + /// Gets a child fully containg the given offset. + /// Goes recursively down the tree. + /// Specail case if at the end of attribute or text + /// </summary> + public AXmlObject GetChildAtOffset(int offset) + { + foreach(AXmlObject child in this.Children) { + if ((child is AXmlAttribute || child is AXmlText) && offset == child.EndOffset) return child; + if (child.StartOffset < offset && offset < child.EndOffset) { + AXmlContainer container = child as AXmlContainer; + if (container != null) { + return container.GetChildAtOffset(offset); + } else { + return child; + } + } + } + return this; // No childs at offset + } + + // Only these four methods should be used to modify the collection + + /// <summary> To be used exlucively by the parser </summary> + internal void AddChild(AXmlObject item) + { + // Childs can be only added to newly parsed items + Assert(this.Parent == null, "I have to be new"); + Assert(item.IsCached, "Added item must be in cache"); + // Do not set parent pointer + this.Children.InsertItemAt(this.Children.Count, item); + } + + /// <summary> To be used exlucively by the parser </summary> + internal void AddChildren(IEnumerable<AXmlObject> items) + { + // Childs can be only added to newly parsed items + Assert(this.Parent == null, "I have to be new"); + // Do not set parent pointer + this.Children.InsertItemsAt(this.Children.Count, items.ToList()); + } + + /// <summary> + /// To be used exclusively by the children update algorithm. + /// Insert child and keep links consistent. + /// </summary> + void InsertChild(int index, AXmlObject item) + { + AXmlParser.Log("Inserting {0} at index {1}", item, index); + + Assert(this.Document != null, "Can not insert to dangling object"); + Assert(item.Parent != this, "Can not own item twice"); + + SetParentPointersInTree(item); + + this.Children.InsertItemAt(index, item); + + this.Document.OnObjectInserted(index, item); + } + + /// <summary> Recursively fix all parent pointer in a tree </summary> + /// <remarks> + /// Cache constraint: + /// If cached item has parent set, then the whole subtree must be consistent and document set + /// </remarks> + void SetParentPointersInTree(AXmlObject item) + { + // All items come from the parser cache + + if (item.Parent == null) { + // Dangling object - either a new parser object or removed tree (still cached) + item.Parent = this; + item.Document = this.Document; + AXmlContainer container = item as AXmlContainer; + if (container != null) { + foreach(AXmlObject child in container.Children) { + container.SetParentPointersInTree(child); + } + } + } else if (item.Parent == this) { + // If node is attached and then deattached, it will have null parent pointer + // but valid subtree - so its children will alredy have correct parent pointer + // like in this case + // item.DebugCheckConsistency(false); + // Rest of the tree is consistent - do not recurse + } else { + // From cache & parent set => consitent subtree + // item.DebugCheckConsistency(false); + // The parent (or any futher parents) can not be part of parsed document + // becuase otherwise this item would be included twice => safe to change parents + // Maintain cache constraint by setting parents to null + foreach(AXmlObject ancest in item.GetAncestors().ToList()) { + ancest.Parent = null; + } + item.Parent = this; + // Rest of the tree is consistent - do not recurse + } + } + + /// <summary> + /// To be used exclusively by the children update algorithm. + /// Remove child, set parent to null and notify the document + /// </summary> + void RemoveChild(int index) + { + AXmlObject removed = this.Children[index]; + AXmlParser.Log("Removing {0} at index {1}", removed, index); + + // Stop tracking if the object can not be used again + if (!removed.IsCached) + this.Document.Parser.TrackedSegments.RemoveParsedObject(removed); + + // Null parent pointer + Assert(removed.Parent == this, "Inconsistent child"); + removed.Parent = null; + + this.Children.RemoveItemAt(index); + + this.Document.OnObjectRemoved(index, removed); + } + + /// <summary> Verify that the subtree is consistent. Only in debug build. </summary> + /// <remarks> Parent pointers might be null or pointing somewhere else in parse tree </remarks> + internal override void DebugCheckConsistency(bool checkParentPointers) + { + base.DebugCheckConsistency(checkParentPointers); + AXmlObject prevChild = null; + int myStartOffset = this.StartOffset; + int myEndOffset = this.EndOffset; + foreach(AXmlObject child in this.Children) { + Assert(child.Length != 0, "Empty child"); + if (checkParentPointers) { + Assert(child.Parent != null, "Null parent reference"); + Assert(child.Parent == this, "Inccorect parent reference"); + } + if (this.Document != null) { + Assert(child.Document != null, "Child has null document"); + Assert(child.Document == this.Document, "Child is in different document"); + } + if (this.IsCached) + Assert(child.IsCached, "Child not in cache"); + Assert(myStartOffset <= child.StartOffset && child.EndOffset <= myEndOffset, "Child not within parent text range"); + if (prevChild != null) + Assert(prevChild.EndOffset <= child.StartOffset, "Overlaping childs"); + child.DebugCheckConsistency(checkParentPointers); + prevChild = child; + } + } + + /// <remarks> + /// Note the the method is not called recuively. + /// Only the helper methods are recursive. + /// </remarks> + internal void UpdateTreeFrom(AXmlContainer srcContainer) + { + this.StartOffset = srcContainer.StartOffset; // Force the update + this.UpdateDataFrom(srcContainer); + RemoveChildrenNotIn(srcContainer.Children); + InsertAndUpdateChildrenFrom(srcContainer.Children); + } + + void RemoveChildrenNotIn(IList<AXmlObject> srcList) + { + Dictionary<int, AXmlObject> srcChildren = srcList.ToDictionary(i => i.StartOffset); + for(int i = 0; i < this.Children.Count;) { + AXmlObject child = this.Children[i]; + AXmlObject srcChild; + + if (srcChildren.TryGetValue(child.StartOffset, out srcChild) && child.CanUpdateDataFrom(srcChild)) { + // Keep only one item with given offset (we might have several due to deletion) + srcChildren.Remove(child.StartOffset); + // If contaner that needs updating + AXmlContainer childAsContainer = child as AXmlContainer; + if (childAsContainer != null && child.LastUpdatedFrom != srcChild) + childAsContainer.RemoveChildrenNotIn(((AXmlContainer)srcChild).Children); + i++; + } else { + RemoveChild(i); + } + } + } + + void InsertAndUpdateChildrenFrom(IList<AXmlObject> srcList) + { + for(int i = 0; i < srcList.Count; i++) { + // End of our list? + if (i == this.Children.Count) { + InsertChild(i, srcList[i]); + continue; + } + AXmlObject child = this.Children[i]; + AXmlObject srcChild = srcList[i]; + + if (child.CanUpdateDataFrom(srcChild)) { // includes offset test + // Does it need updating? + if (child.LastUpdatedFrom != srcChild) { + child.UpdateDataFrom(srcChild); + AXmlContainer childAsContainer = child as AXmlContainer; + if (childAsContainer != null) + childAsContainer.InsertAndUpdateChildrenFrom(((AXmlContainer)srcChild).Children); + } + } else { + InsertChild(i, srcChild); + } + } + Assert(this.Children.Count == srcList.Count, "List lengths differ after update"); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlDocument.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlDocument.cs new file mode 100644 index 000000000..f96c50ca4 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlDocument.cs @@ -0,0 +1,69 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// The root object of the XML document + /// </summary> + public class AXmlDocument: AXmlContainer + { + /// <summary> Parser that produced this document </summary> + internal AXmlParser Parser { get; set; } + + /// <summary> Occurs when object is added to any part of the document </summary> + public event EventHandler<NotifyCollectionChangedEventArgs> ObjectInserted; + /// <summary> Occurs when object is removed from any part of the document </summary> + public event EventHandler<NotifyCollectionChangedEventArgs> ObjectRemoved; + /// <summary> Occurs before local data of any object in the document changes </summary> + public event EventHandler<AXmlObjectEventArgs> ObjectChanging; + /// <summary> Occurs after local data of any object in the document changed </summary> + public event EventHandler<AXmlObjectEventArgs> ObjectChanged; + + internal void OnObjectInserted(int index, AXmlObject obj) + { + if (ObjectInserted != null) + ObjectInserted(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new AXmlObject[] { obj }.ToList(), index)); + } + + internal void OnObjectRemoved(int index, AXmlObject obj) + { + if (ObjectRemoved != null) + ObjectRemoved(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new AXmlObject[] { obj }.ToList(), index)); + } + + internal void OnObjectChanging(AXmlObject obj) + { + if (ObjectChanging != null) + ObjectChanging(this, new AXmlObjectEventArgs() { Object = obj } ); + } + + internal void OnObjectChanged(AXmlObject obj) + { + if (ObjectChanged != null) + ObjectChanged(this, new AXmlObjectEventArgs() { Object = obj } ); + } + + /// <inheritdoc/> + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitDocument(this); + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} Chld:{1}]", base.ToString(), this.Children.Count); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlElement.cs new file mode 100644 index 000000000..080d538e8 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlElement.cs @@ -0,0 +1,226 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Logical grouping of other nodes together. + /// </summary> + public class AXmlElement: AXmlContainer + { + /// <summary> No tags are missing anywhere within this element (recursive) </summary> + public bool IsProperlyNested { get; set; } + /// <returns> True in wellformed XML </returns> + public bool HasStartOrEmptyTag { get; set; } + /// <returns> True in wellformed XML </returns> + public bool HasEndTag { get; set; } + + /// <inheritdoc/> + internal override bool UpdateDataFrom(AXmlObject source) + { + if (!base.UpdateDataFrom(source)) return false; + AXmlElement src = (AXmlElement)source; + // Clear the cache for this - quite expensive + attributesAndElements = null; + if (this.IsProperlyNested != src.IsProperlyNested || + this.HasStartOrEmptyTag != src.HasStartOrEmptyTag || + this.HasEndTag != src.HasEndTag) + { + OnChanging(); + this.IsProperlyNested = src.IsProperlyNested; + this.HasStartOrEmptyTag = src.HasStartOrEmptyTag; + this.HasEndTag = src.HasEndTag; + OnChanged(); + return true; + } else { + return false; + } + } + + /// <summary> The start or empty-element tag if there is any </summary> + internal AXmlTag StartTag { + get { + Assert(HasStartOrEmptyTag, "Does not have a start tag"); + return (AXmlTag)this.Children[0]; + } + } + + /// <summary> The end tag if there is any </summary> + internal AXmlTag EndTag { + get { + Assert(HasEndTag, "Does not have an end tag"); + return (AXmlTag)this.Children[this.Children.Count - 1]; + } + } + + internal override void DebugCheckConsistency(bool checkParentPointers) + { + DebugAssert(Children.Count > 0, "No children"); + base.DebugCheckConsistency(checkParentPointers); + } + + #region Helpper methods + + /// <summary> Gets attributes of the element </summary> + /// <remarks> + /// Warning: this is a cenvenience method to access the attributes of the start tag. + /// However, since the start tag might be moved/replaced, this property might return + /// different values over time. + /// </remarks> + public AXmlAttributeCollection Attributes { + get { + if (this.HasStartOrEmptyTag) { + return this.StartTag.Attributes; + } else { + return AXmlAttributeCollection.Empty; + } + } + } + + ObservableCollection<AXmlObject> attributesAndElements; + + /// <summary> Gets both attributes and elements. Expensive, avoid use. </summary> + /// <remarks> Warning: the collection will regenerate after each update </remarks> + public ObservableCollection<AXmlObject> AttributesAndElements { + get { + if (attributesAndElements == null) { + if (this.HasStartOrEmptyTag) { + attributesAndElements = new MergedCollection<AXmlObject, ObservableCollection<AXmlObject>> ( + // New wrapper with RawObject types + new FilteredCollection<AXmlObject, AXmlObjectCollection<AXmlObject>>(this.StartTag.Children, x => x is AXmlAttribute), + new FilteredCollection<AXmlObject, AXmlObjectCollection<AXmlObject>>(this.Children, x => x is AXmlElement) + ); + } else { + attributesAndElements = new FilteredCollection<AXmlObject, AXmlObjectCollection<AXmlObject>>(this.Children, x => x is AXmlElement); + } + } + return attributesAndElements; + } + } + + /// <summary> Name with namespace prefix - exactly as in source </summary> + public string Name { + get { + if (this.HasStartOrEmptyTag) { + return this.StartTag.Name; + } else { + return this.EndTag.Name; + } + } + } + + /// <summary> The part of name before ":" </summary> + /// <returns> Empty string if not found </returns> + public string Prefix { + get { + return GetNamespacePrefix(this.Name); + } + } + + /// <summary> The part of name after ":" </summary> + /// <returns> Empty string if not found </returns> + public string LocalName { + get { + return GetLocalName(this.Name); + } + } + + /// <summary> Resolved namespace of the name </summary> + /// <returns> Empty string if prefix is not found </returns> + public string Namespace { + get { + string prefix = this.Prefix; + if (string.IsNullOrEmpty(prefix)) { + return FindDefaultNamespace(); + } else { + return ResolvePrefix(prefix); + } + } + } + + /// <summary> Find the defualt namespace for this context </summary> + public string FindDefaultNamespace() + { + AXmlElement current = this; + while(current != null) { + string namesapce = current.GetAttributeValue(NoNamespace, "xmlns"); + if (namesapce != null) return namesapce; + current = current.Parent as AXmlElement; + } + return string.Empty; // No namesapce + } + + /// <summary> + /// Recursively resolve given prefix in this context. Prefix must have some value. + /// </summary> + /// <returns> Empty string if prefix is not found </returns> + public string ResolvePrefix(string prefix) + { + if (string.IsNullOrEmpty(prefix)) throw new ArgumentException("No prefix given", "prefix"); + + // Implicit namesapces + if (prefix == "xml") return XmlNamespace; + if (prefix == "xmlns") return XmlnsNamespace; + + AXmlElement current = this; + while(current != null) { + string namesapce = current.GetAttributeValue(XmlnsNamespace, prefix); + if (namesapce != null) return namesapce; + current = current.Parent as AXmlElement; + } + return NoNamespace; // Can not find prefix + } + + /// <summary> + /// Get unquoted value of attribute. + /// It looks in the no namespace (empty string). + /// </summary> + /// <returns>Null if not found</returns> + public string GetAttributeValue(string localName) + { + return GetAttributeValue(NoNamespace, localName); + } + + /// <summary> + /// Get unquoted value of attribute + /// </summary> + /// <param name="namespace">Namespace. Can be no namepace (empty string), which is the default for attributes.</param> + /// <param name="localName">Local name - text after ":"</param> + /// <returns>Null if not found</returns> + public string GetAttributeValue(string @namespace, string localName) + { + @namespace = @namespace ?? string.Empty; + foreach(AXmlAttribute attr in this.Attributes.GetByLocalName(localName)) { + DebugAssert(attr.LocalName == localName, "Bad hashtable"); + if (attr.Namespace == @namespace) { + return attr.Value; + } + } + return null; + } + + #endregion + + /// <inheritdoc/> + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitElement(this); + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} '{1}' Attr:{2} Chld:{3} Nest:{4}]", base.ToString(), this.Name, this.HasStartOrEmptyTag ? this.StartTag.Children.Count : 0, this.Children.Count, this.IsProperlyNested ? "Ok" : "Bad"); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObject.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObject.cs new file mode 100644 index 000000000..4951a7c0b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObject.cs @@ -0,0 +1,266 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Abstact base class for all types + /// </summary> + public abstract class AXmlObject: TextSegment + { + /// <summary> Empty string. The namespace used if there is no "xmlns" specified </summary> + public static readonly string NoNamespace = string.Empty; + + /// <summary> Namespace for "xml:" prefix: "http://www.w3.org/XML/1998/namespace" </summary> + public static readonly string XmlNamespace = "http://www.w3.org/XML/1998/namespace"; + + /// <summary> Namesapce for "xmlns:" prefix: "http://www.w3.org/2000/xmlns/" </summary> + public static readonly string XmlnsNamespace = "http://www.w3.org/2000/xmlns/"; + + /// <summary> Parent node. </summary> + /// <remarks> + /// New cached items start with null parent. + /// Cache constraint: + /// If cached item has parent set, then the whole subtree must be consistent + /// </remarks> + public AXmlObject Parent { get; set; } + + /// <summary> + /// Gets the document that has owns this object. + /// Once set, it is not changed. Not even set to null. + /// </summary> + internal AXmlDocument Document { get; set; } + + /// <summary> Creates new object </summary> + protected AXmlObject() + { + this.LastUpdatedFrom = this; + } + + /// <summary> Occurs before the value of any local properties changes. Nested changes do not cause the event to occur </summary> + public event EventHandler<AXmlObjectEventArgs> Changing; + + /// <summary> Occurs after the value of any local properties changed. Nested changes do not cause the event to occur </summary> + public event EventHandler<AXmlObjectEventArgs> Changed; + + /// <summary> Raises Changing event </summary> + protected void OnChanging() + { + AXmlParser.Log("Changing {0}", this); + if (Changing != null) { + Changing(this, new AXmlObjectEventArgs() { Object = this } ); + } + AXmlDocument doc = this.Document; + if (doc != null) { + doc.OnObjectChanging(this); + } + // As a convenience, also rasie an event for the parent element + AXmlTag me = this as AXmlTag; + if (me != null && (me.IsStartOrEmptyTag || me.IsEndTag) && me.Parent is AXmlElement) { + me.Parent.OnChanging(); + } + } + + /// <summary> Raises Changed event </summary> + protected void OnChanged() + { + AXmlParser.Log("Changed {0}", this); + if (Changed != null) { + Changed(this, new AXmlObjectEventArgs() { Object = this } ); + } + AXmlDocument doc = this.Document; + if (doc != null) { + doc.OnObjectChanged(this); + } + // As a convenience, also rasie an event for the parent element + AXmlTag me = this as AXmlTag; + if (me != null && (me.IsStartOrEmptyTag || me.IsEndTag) && me.Parent is AXmlElement) { + me.Parent.OnChanged(); + } + } + + List<SyntaxError> syntaxErrors; + + /// <summary> + /// The error that occured in the context of this node (excluding nested nodes) + /// </summary> + public IEnumerable<SyntaxError> MySyntaxErrors { + get { + if (syntaxErrors == null) { + return new SyntaxError[] {}; + } else { + return syntaxErrors; + } + } + } + + /// <summary> + /// The error that occured in the context of this node and all nested nodes. + /// It has O(n) cost. + /// </summary> + public IEnumerable<SyntaxError> SyntaxErrors { + get { + return GetSelfAndAllChildren().SelectMany(obj => obj.MySyntaxErrors); + } + } + + internal void AddSyntaxError(SyntaxError error) + { + DebugAssert(error.Object == this, "Must own the error"); + if (this.syntaxErrors == null) this.syntaxErrors = new List<SyntaxError>(); + syntaxErrors.Add(error); + } + + /// <summary> Throws exception if condition is false </summary> + /// <remarks> Present in release mode - use only for very cheap aserts </remarks> + protected static void Assert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + /// <summary> Throws exception if condition is false </summary> + [Conditional("DEBUG")] + protected static void DebugAssert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + /// <summary> Recursively gets self and all nested nodes. </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", + Justification = "Using a method makes the API look more LINQ-like and indicates that the returned collection is computed every time.")] + public virtual IEnumerable<AXmlObject> GetSelfAndAllChildren() + { + return new AXmlObject[] { this }; + } + + /// <summary> Get all ancestors of this node </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", + Justification = "Using a method makes the API look more LINQ-like and indicates that the returned collection is computed every time.")] + public IEnumerable<AXmlObject> GetAncestors() + { + AXmlObject curr = this.Parent; + while(curr != null) { + yield return curr; + curr = curr.Parent; + } + } + + /// <summary> Call appropriate visit method on the given visitor </summary> + public abstract void AcceptVisitor(IAXmlVisitor visitor); + + /// <summary> The parser tree object this object was updated from </summary> + /// <remarks> Initialized to 'this' </remarks> + internal AXmlObject LastUpdatedFrom { get; private set; } + + internal bool IsCached { get; set; } + + /// <summary> Is call to UpdateDataFrom is allowed? </summary> + internal bool CanUpdateDataFrom(AXmlObject source) + { + return + this.GetType() == source.GetType() && + this.StartOffset == source.StartOffset && + (this.LastUpdatedFrom == source || !this.IsCached); + } + + /// <summary> Copy all data from the 'source' to this object </summary> + /// <remarks> Returns true if any updates were done </remarks> + internal virtual bool UpdateDataFrom(AXmlObject source) + { + Assert(this.GetType() == source.GetType(), "Source has different type"); + DebugAssert(this.StartOffset == source.StartOffset, "Source has different StartOffset"); + + if (this.LastUpdatedFrom == source) { + DebugAssert(this.EndOffset == source.EndOffset, "Source has different EndOffset"); + return false; + } + + Assert(!this.IsCached, "Can not update cached item"); + Assert(source.IsCached, "Must update from cache"); + + this.LastUpdatedFrom = source; + this.StartOffset = source.StartOffset; + // In some cases we are just updating objects of that same + // type and position and hoping to be luckily right + this.EndOffset = source.EndOffset; + + // Do not bother comparing - assume changed if non-null + if (this.syntaxErrors != null || source.syntaxErrors != null) { + // May be called again in derived class - oh, well, does not matter + OnChanging(); + this.Document.Parser.TrackedSegments.RemoveSyntaxErrorsOf(this); + if (source.syntaxErrors == null) { + this.syntaxErrors = null; + } else { + this.syntaxErrors = new List<SyntaxError>(); + foreach(var error in source.MySyntaxErrors) { + // The object differs, so create our own copy + // The source still might need it in the future and we do not want to break it + this.AddSyntaxError(error.Clone(this)); + } + } + this.Document.Parser.TrackedSegments.AddSyntaxErrorsOf(this); + OnChanged(); + } + + return true; + } + + /// <summary> Verify that the item is consistent. Only in debug build. </summary> + [Conditional("DEBUG")] + internal virtual void DebugCheckConsistency(bool allowNullParent) + { + + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}({1}-{2})", this.GetType().Name.Remove(0, 4), this.StartOffset, this.EndOffset); + } + + #region Helpper methods + + /// <summary> The part of name before ":" </summary> + /// <returns> Empty string if not found </returns> + protected static string GetNamespacePrefix(string name) + { + if (string.IsNullOrEmpty(name)) return string.Empty; + int colonIndex = name.IndexOf(':'); + if (colonIndex != -1) { + return name.Substring(0, colonIndex); + } else { + return string.Empty; + } + } + + /// <summary> The part of name after ":" </summary> + /// <returns> Whole name if ":" not found </returns> + protected static string GetLocalName(string name) + { + if (string.IsNullOrEmpty(name)) return string.Empty; + int colonIndex = name.IndexOf(':'); + if (colonIndex != -1) { + return name.Remove(0, colonIndex + 1); + } else { + return name ?? string.Empty; + } + } + + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectCollection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectCollection.cs new file mode 100644 index 000000000..3a2c7cc21 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectCollection.cs @@ -0,0 +1,90 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Collection that is publicly read-only and has support + /// for adding/removing multiple items at a time. + /// </summary> + public class AXmlObjectCollection<T>: Collection<T>, INotifyCollectionChanged + { + /// <summary> Occurs when the collection is changed </summary> + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// <summary> Raises <see cref="CollectionChanged"/> event </summary> + // Do not inherit - it is not called if event is null + void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (CollectionChanged != null) { + CollectionChanged(this, e); + } + } + + /// <inheritdoc/> + protected override void ClearItems() + { + throw new NotSupportedException(); + } + + /// <inheritdoc/> + protected override void InsertItem(int index, T item) + { + throw new NotSupportedException(); + } + + /// <inheritdoc/> + protected override void RemoveItem(int index) + { + throw new NotSupportedException(); + } + + /// <inheritdoc/> + protected override void SetItem(int index, T item) + { + throw new NotSupportedException(); + } + + internal void InsertItemAt(int index, T item) + { + base.InsertItem(index, item); + if (CollectionChanged != null) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new T[] { item }.ToList(), index)); + } + + internal void RemoveItemAt(int index) + { + T removed = this[index]; + base.RemoveItem(index); + if (CollectionChanged != null) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new T[] { removed }.ToList(), index)); + } + + internal void InsertItemsAt(int index, IList<T> items) + { + for(int i = 0; i < items.Count; i++) { + base.InsertItem(index + i, items[i]); + } + if (CollectionChanged != null) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, (IList)items, index)); + } + + internal void RemoveItemsAt(int index, int count) + { + List<T> removed = new List<T>(); + for(int i = 0; i < count; i++) { + removed.Add(this[index]); + base.RemoveItem(index); + } + if (CollectionChanged != null) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, (IList)removed, index)); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectEventArgs.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectEventArgs.cs new file mode 100644 index 000000000..27ea041e6 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectEventArgs.cs @@ -0,0 +1,21 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> Holds event args for event caused by <see cref="AXmlObject"/> </summary> + public class AXmlObjectEventArgs: EventArgs + { + /// <summary> The object that caused the event </summary> + public AXmlObject Object { get; set; } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlParser.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlParser.cs new file mode 100644 index 000000000..625ff59d9 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlParser.cs @@ -0,0 +1,201 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Creates object tree from XML document. + /// </summary> + /// <remarks> + /// The created tree fully describes the document and thus the orginal XML file can be + /// exactly reproduced. + /// + /// Any further parses will reparse only the changed parts and the existing tree will + /// be updated with the changes. The user can add event handlers to be notified of + /// the changes. The parser tries to minimize the number of changes to the tree. + /// (for example, it will add a single child at the start of collection rather than + /// clearing the collection and adding new children) + /// + /// The object tree consists of following types: + /// RawObject - Abstact base class for all types + /// RawContainer - Abstact base class for all types that can contain child nodes + /// RawDocument - The root object of the XML document + /// RawElement - Logical grouping of other nodes together. The first child is always the start tag. + /// RawTag - Represents any markup starting with "<" and (hopefully) ending with ">" + /// RawAttribute - Name-value pair in a tag + /// RawText - Whitespace or character data + /// + /// For example, see the following XML and the produced object tree: + /// <![CDATA[ + /// <!-- My favourite quote --> + /// <quote author="Albert Einstein"> + /// Make everything as simple as possible, but not simpler. + /// </quote> + /// + /// RawDocument + /// RawTag "<!--" "-->" + /// RawText " My favourite quote " + /// RawElement + /// RawTag "<" "quote" ">" + /// RawText " " + /// RawAttribute 'author="Albert Einstein"' + /// RawText "\n Make everything as simple as possible, but not simpler.\n" + /// RawTag "</" "quote" ">" + /// ]]> + /// + /// The precise content of RawTag depends on what it represents: + /// <![CDATA[ + /// Start tag: "<" Name? (RawText+ RawAttribute)* RawText* (">" | "/>") + /// End tag: "</" Name? (RawText+ RawAttribute)* RawText* ">" + /// P.instr.: "<?" Name? (RawText)* "?>" + /// Comment: "<!--" (RawText)* "-->" + /// CData: "<![CDATA[" (RawText)* "]]" ">" + /// DTD: "<!DOCTYPE" (RawText+ RawTag)* RawText* ">" (DOCTYPE or other DTD names) + /// UknownBang: "<!" (RawText)* ">" + /// ]]> + /// + /// The type of tag can be identified by the opening backet. + /// There are helpper properties in the RawTag class to identify the type, exactly + /// one of the properties will be true. + /// + /// The closing bracket may be missing or may be different for mallformed XML. + /// + /// Note that there can always be multiple consequtive RawText nodes. + /// This is to ensure that idividual texts are not too long. + /// + /// XML Spec: http://www.w3.org/TR/xml/ + /// XML EBNF: http://www.jelks.nu/XML/xmlebnf.html + /// + /// Internals: + /// + /// "Try" methods can silently fail by returning false. + /// MoveTo methods do not move if they are already at the given target + /// If methods return some object, it must be no-empty. It is up to the caller to ensure + /// the context is appropriate for reading. + /// + /// </remarks> + public class AXmlParser + { + AXmlDocument userDocument; + + internal TrackedSegmentCollection TrackedSegments { get; private set; } + + /// <summary> + /// Generate syntax error when seeing enity reference other then the build-in ones + /// </summary> + public bool UnknownEntityReferenceIsError { get; set; } + + /// <summary> Create new parser </summary> + public AXmlParser() + { + this.Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + ClearInternal(); + } + + /// <summary> Throws exception if condition is false </summary> + internal static void Assert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + /// <summary> Throws exception if condition is false </summary> + [Conditional("DEBUG")] + internal static void DebugAssert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + [Conditional("DEBUG")] + internal static void Log(string text, params object[] pars) + { + //System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "XML: " + text, pars)); + } + + /// <summary> + /// Incrementaly parse the given text. + /// You have to hold the write lock. + /// </summary> + /// <param name="input"> + /// The full XML text of the new document. + /// </param> + /// <param name="changesSinceLastParse"> + /// Changes since last parse. Null will cause full reparse. + /// </param> + public AXmlDocument Parse(string input, IEnumerable<DocumentChangeEventArgs> changesSinceLastParse) + { + if (!Lock.IsWriteLockHeld) + throw new InvalidOperationException("Lock needed!"); + + // Use changes to invalidate cache + if (changesSinceLastParse != null) { + this.TrackedSegments.UpdateOffsetsAndInvalidate(changesSinceLastParse); + } else { + this.TrackedSegments.InvalidateAll(); + } + + TagReader tagReader = new TagReader(this, input); + List<AXmlObject> tags = tagReader.ReadAllTags(); + AXmlDocument parsedDocument = new TagMatchingHeuristics(this, input, tags).ReadDocument(); + tagReader.PrintStringCacheStats(); + AXmlParser.Log("Updating main DOM tree..."); + userDocument.UpdateTreeFrom(parsedDocument); + userDocument.DebugCheckConsistency(true); + Assert(userDocument.GetSelfAndAllChildren().Count() == parsedDocument.GetSelfAndAllChildren().Count(), "Parsed document and updated document have different number of children"); + return userDocument; + } + + /// <summary> + /// Makes calls to Parse() thread-safe. Use Lock everywhere Parse() is called. + /// </summary> + public ReaderWriterLockSlim Lock { get; private set; } + + /// <summary> + /// Returns the last cached version of the document. + /// </summary> + /// <exception cref="InvalidOperationException">No read lock is held by the current thread.</exception> + public AXmlDocument LastDocument { + get { + if (!Lock.IsReadLockHeld) + throw new InvalidOperationException("Read lock needed!"); + + return userDocument; + } + } + + /// <summary> + /// Clears the parser data. + /// </summary> + /// <exception cref="InvalidOperationException">No write lock is held by the current thread.</exception> + public void Clear() + { + if (!Lock.IsWriteLockHeld) + throw new InvalidOperationException("Write lock needed!"); + + ClearInternal(); + } + + void ClearInternal() + { + this.UnknownEntityReferenceIsError = true; + this.TrackedSegments = new TrackedSegmentCollection(); + this.userDocument = new AXmlDocument() { Parser = this }; + this.userDocument.Document = this.userDocument; + // Track the document + this.TrackedSegments.AddParsedObject(this.userDocument, null); + this.userDocument.IsCached = false; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlTag.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlTag.cs new file mode 100644 index 000000000..8283cea22 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlTag.cs @@ -0,0 +1,108 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Represents any markup starting with "<" and (hopefully) ending with ">" + /// </summary> + public class AXmlTag: AXmlContainer + { + /// <summary> These identify the start of DTD elements </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification="ReadOnlyCollection is immutable")] + public static readonly ReadOnlyCollection<string> DtdNames = new ReadOnlyCollection<string>( + new string[] {"<!DOCTYPE", "<!NOTATION", "<!ELEMENT", "<!ATTLIST", "<!ENTITY" } ); + + /// <summary> Opening bracket - usually "<" </summary> + public string OpeningBracket { get; internal set; } + /// <summary> Name following the opening bracket </summary> + public string Name { get; internal set; } + /// <summary> Opening bracket - usually ">" </summary> + public string ClosingBracket { get; internal set; } + + /// <summary> True if tag starts with "<" </summary> + public bool IsStartOrEmptyTag { get { return OpeningBracket == "<"; } } + /// <summary> True if tag starts with "<" and ends with ">" </summary> + public bool IsStartTag { get { return OpeningBracket == "<" && ClosingBracket == ">"; } } + /// <summary> True if tag starts with "<" and does not end with ">" </summary> + public bool IsEmptyTag { get { return OpeningBracket == "<" && ClosingBracket != ">" ; } } + /// <summary> True if tag starts with "</" </summary> + public bool IsEndTag { get { return OpeningBracket == "</"; } } + /// <summary> True if tag starts with "<?" </summary> + public bool IsProcessingInstruction { get { return OpeningBracket == "<?"; } } + /// <summary> True if tag starts with "<!--" </summary> + public bool IsComment { get { return OpeningBracket == "<!--"; } } + /// <summary> True if tag starts with "<![CDATA[" </summary> + public bool IsCData { get { return OpeningBracket == "<![CDATA["; } } + /// <summary> True if tag starts with one of the DTD starts </summary> + public bool IsDocumentType { get { return DtdNames.Contains(OpeningBracket); } } + /// <summary> True if tag starts with "<!" </summary> + public bool IsUnknownBang { get { return OpeningBracket == "<!"; } } + + #region Helpper methods + + AXmlAttributeCollection attributes; + + /// <summary> Gets attributes of the tag (if applicable) </summary> + public AXmlAttributeCollection Attributes { + get { + if (attributes == null) { + attributes = new AXmlAttributeCollection(this.Children); + } + return attributes; + } + } + + #endregion + + internal override void DebugCheckConsistency(bool checkParentPointers) + { + Assert(OpeningBracket != null, "Null OpeningBracket"); + Assert(Name != null, "Null Name"); + Assert(ClosingBracket != null, "Null ClosingBracket"); + base.DebugCheckConsistency(checkParentPointers); + } + + /// <inheritdoc/> + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitTag(this); + } + + /// <inheritdoc/> + internal override bool UpdateDataFrom(AXmlObject source) + { + if (!base.UpdateDataFrom(source)) return false; + AXmlTag src = (AXmlTag)source; + if (this.OpeningBracket != src.OpeningBracket || + this.Name != src.Name || + this.ClosingBracket != src.ClosingBracket) + { + OnChanging(); + this.OpeningBracket = src.OpeningBracket; + this.Name = src.Name; + this.ClosingBracket = src.ClosingBracket; + OnChanged(); + return true; + } else { + return false; + } + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} '{1}{2}{3}' Attr:{4}]", base.ToString(), this.OpeningBracket, this.Name, this.ClosingBracket, this.Children.Count); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlText.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlText.cs new file mode 100644 index 000000000..8470979ce --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlText.cs @@ -0,0 +1,62 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Whitespace or character data + /// </summary> + public class AXmlText: AXmlObject + { + /// <summary> The context in which the text occured </summary> + internal TextType Type { get; set; } + /// <summary> The text exactly as in source </summary> + public string EscapedValue { get; set; } + /// <summary> The text with all entity references resloved </summary> + public string Value { get; set; } + /// <summary> True if the text contains only whitespace characters </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace", + Justification = "System.Xml also uses 'Whitespace'")] + public bool ContainsOnlyWhitespace { get; set; } + + /// <inheritdoc/> + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitText(this); + } + + /// <inheritdoc/> + internal override bool UpdateDataFrom(AXmlObject source) + { + if (!base.UpdateDataFrom(source)) return false; + AXmlText src = (AXmlText)source; + if (this.EscapedValue != src.EscapedValue || + this.Value != src.Value) + { + OnChanging(); + this.EscapedValue = src.EscapedValue; + this.Value = src.Value; + OnChanged(); + return true; + } else { + return false; + } + } + + /// <inheritdoc/> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[{0} Text.Length={1}]", base.ToString(), this.EscapedValue.Length); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AbstractAXmlVisitor.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AbstractAXmlVisitor.cs new file mode 100644 index 000000000..1c8e72bbb --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AbstractAXmlVisitor.cs @@ -0,0 +1,44 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Text; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Derive from this class to create visitor for the XML tree + /// </summary> + public abstract class AbstractAXmlVisitor : IAXmlVisitor + { + /// <summary> Visit RawDocument </summary> + public virtual void VisitDocument(AXmlDocument document) + { + foreach(AXmlObject child in document.Children) child.AcceptVisitor(this); + } + + /// <summary> Visit RawElement </summary> + public virtual void VisitElement(AXmlElement element) + { + foreach(AXmlObject child in element.Children) child.AcceptVisitor(this); + } + + /// <summary> Visit RawTag </summary> + public virtual void VisitTag(AXmlTag tag) + { + foreach(AXmlObject child in tag.Children) child.AcceptVisitor(this); + } + + /// <summary> Visit RawAttribute </summary> + public virtual void VisitAttribute(AXmlAttribute attribute) + { + + } + + /// <summary> Visit RawText </summary> + public virtual void VisitText(AXmlText text) + { + + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/CanonicalPrintAXmlVisitor.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/CanonicalPrintAXmlVisitor.cs new file mode 100644 index 000000000..df2ac5e81 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/CanonicalPrintAXmlVisitor.cs @@ -0,0 +1,119 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Linq; +using System.Text; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Converts the XML tree back to text in canonical form. + /// See http://www.w3.org/TR/xml-c14n + /// </summary> + public class CanonicalPrintAXmlVisitor: AbstractAXmlVisitor + { + StringBuilder sb = new StringBuilder(); + + /// <summary> + /// Gets the pretty printed text + /// </summary> + public string Output { + get { + return sb.ToString(); + } + } + + /// <summary> Create canonical text from a document </summary> + public static string Print(AXmlDocument doc) + { + CanonicalPrintAXmlVisitor visitor = new CanonicalPrintAXmlVisitor(); + visitor.VisitDocument(doc); + return visitor.Output; + } + + /// <summary> Visit RawDocument </summary> + public override void VisitDocument(AXmlDocument document) + { + foreach(AXmlObject child in document.Children) { + AXmlTag childAsTag = child as AXmlTag; + // Only procssing instructions or elements + if (childAsTag != null && childAsTag.IsProcessingInstruction && childAsTag.Name != "xml") { + VisitTag(childAsTag); + } else { + AXmlElement childAsElement = child as AXmlElement; + if (childAsElement != null) { + VisitElement(childAsElement); + } + } + } + } + + /// <summary> Visit RawElement </summary> + public override void VisitElement(AXmlElement element) + { + base.VisitElement(element); + } + + /// <summary> Visit RawTag </summary> + public override void VisitTag(AXmlTag tag) + { + if (tag.IsStartOrEmptyTag) { + sb.Append('<'); + sb.Append(tag.Name); + foreach(AXmlAttribute attr in tag.Children.OfType<AXmlAttribute>().OrderBy(a => a.Name)) { + VisitAttribute(attr); + } + sb.Append('>'); + if (tag.IsEmptyTag) { + // Use explicit start-end pair + sb.AppendFormat("</{0}>", tag.Name); + } + } else if (tag.IsEndTag) { + sb.AppendFormat("</{0}>", tag.Name); + } else if (tag.IsProcessingInstruction) { + sb.Append("<?"); + sb.Append(tag.Name); + foreach(AXmlText text in tag.Children.OfType<AXmlText>()) { + sb.Append(text.Value); + } + if (tag.Children.Count == 0) + sb.Append(' '); + sb.Append("?>"); + } else if (tag.IsCData) { + foreach(AXmlText text in tag.Children.OfType<AXmlText>()) { + sb.Append(Escape(text.Value)); + } + } + } + + /// <summary> Visit RawAttribute </summary> + public override void VisitAttribute(AXmlAttribute attribute) + { + sb.Append(' '); + sb.Append(attribute.Name); + sb.Append("="); + sb.Append('"'); + sb.Append(Escape(attribute.Value)); + sb.Append('"'); + } + + /// <summary> Visit RawText </summary> + public override void VisitText(AXmlText text) + { + sb.Append(Escape(text.Value)); + } + + static string Escape(string text) + { + return text + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("\u0009", "	") + .Replace("\u000A", " ") + .Replace("\u000D", " "); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/ExtensionMethods.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/ExtensionMethods.cs new file mode 100644 index 000000000..34664a23a --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/ExtensionMethods.cs @@ -0,0 +1,47 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + static class ExtensionMethods + { + // Copied from ICSharpCode.SharpDevelop.Dom.ExtensionMethods + /// <summary> + /// Converts a recursive data structure into a flat list. + /// </summary> + /// <param name="input">The root elements of the recursive data structure.</param> + /// <param name="recursion">The function that gets the children of an element.</param> + /// <returns>Iterator that enumerates the tree structure in preorder.</returns> + public static IEnumerable<T> Flatten<T>(this IEnumerable<T> input, Func<T, IEnumerable<T>> recursion) + { + Stack<IEnumerator<T>> stack = new Stack<IEnumerator<T>>(); + try { + stack.Push(input.GetEnumerator()); + while (stack.Count > 0) { + while (stack.Peek().MoveNext()) { + T element = stack.Peek().Current; + yield return element; + IEnumerable<T> children = recursion(element); + if (children != null) { + stack.Push(children.GetEnumerator()); + } + } + stack.Pop().Dispose(); + } + } finally { + while (stack.Count > 0) { + stack.Pop().Dispose(); + } + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/FilteredCollection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/FilteredCollection.cs new file mode 100644 index 000000000..fd936d1b0 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/FilteredCollection.cs @@ -0,0 +1,99 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Collection that presents only some items from the wrapped collection. + /// It implicitely filters object that are not of type T (or derived). + /// </summary> + public class FilteredCollection<T, TCollection>: ObservableCollection<T> where TCollection : INotifyCollectionChanged, IList + { + TCollection source; + Predicate<object> condition; + List<int> srcPtrs = new List<int>(); // Index to the original collection + + /// <summary> Create unbound collection </summary> + protected FilteredCollection() {} + + /// <summary> Wrap the given collection. Items of type other then T are filtered </summary> + public FilteredCollection(TCollection source) : this (source, x => true) { } + + /// <summary> Wrap the given collection. Items of type other then T are filtered. Items not matching the condition are filtered. </summary> + public FilteredCollection(TCollection source, Predicate<object> condition) + { + this.source = source; + this.condition = condition; + + this.source.CollectionChanged += SourceCollectionChanged; + + Reset(); + } + + void Reset() + { + this.Clear(); + srcPtrs.Clear(); + for(int i = 0; i < source.Count; i++) { + if (source[i] is T && condition(source[i])) { + this.Add((T)source[i]); + srcPtrs.Add(i); + } + } + } + + void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch(e.Action) { + case NotifyCollectionChangedAction.Add: + // Update pointers + for(int i = 0; i < srcPtrs.Count; i++) { + if (srcPtrs[i] >= e.NewStartingIndex) { + srcPtrs[i] += e.NewItems.Count; + } + } + // Find where to add items + int addIndex = srcPtrs.FindIndex(srcPtr => srcPtr >= e.NewStartingIndex); + if (addIndex == -1) addIndex = this.Count; + // Add items to collection + for(int i = 0; i < e.NewItems.Count; i++) { + if (e.NewItems[i] is T && condition(e.NewItems[i])) { + this.InsertItem(addIndex, (T)e.NewItems[i]); + srcPtrs.Insert(addIndex, e.NewStartingIndex + i); + addIndex++; + } + } + break; + case NotifyCollectionChangedAction.Remove: + // Remove the item from our collection + for(int i = 0; i < e.OldItems.Count; i++) { + // Anyone points to the removed item? + int removeIndex = srcPtrs.IndexOf(e.OldStartingIndex + i); + // Remove + if (removeIndex != -1) { + this.RemoveAt(removeIndex); + srcPtrs.RemoveAt(removeIndex); + } + } + // Update pointers + for(int i = 0; i < srcPtrs.Count; i++) { + if (srcPtrs[i] >= e.OldStartingIndex) { + srcPtrs[i] -= e.OldItems.Count; + } + } + break; + case NotifyCollectionChangedAction.Reset: + Reset(); + break; + default: + throw new NotSupportedException(e.Action.ToString()); + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/IAXmlVisitor.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/IAXmlVisitor.cs new file mode 100644 index 000000000..cbd4b2549 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/IAXmlVisitor.cs @@ -0,0 +1,29 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Text; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Visitor for the XML tree + /// </summary> + public interface IAXmlVisitor + { + /// <summary> Visit RawDocument </summary> + void VisitDocument(AXmlDocument document); + + /// <summary> Visit RawElement </summary> + void VisitElement(AXmlElement element); + + /// <summary> Visit RawTag </summary> + void VisitTag(AXmlTag tag); + + /// <summary> Visit RawAttribute </summary> + void VisitAttribute(AXmlAttribute attribute); + + /// <summary> Visit RawText </summary> + void VisitText(AXmlText text); + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/InternalException.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/InternalException.cs new file mode 100644 index 000000000..a52ac4b4f --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/InternalException.cs @@ -0,0 +1,45 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Runtime.Serialization; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Exception used for internal errors in XML parser. + /// This exception indicates a bug in AvalonEdit. + /// </summary> + [Serializable()] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1064:ExceptionsShouldBePublic", Justification = "This exception is not public because it is not supposed to be caught by user code - it indicates a bug in AvalonEdit.")] + class InternalException : Exception + { + /// <summary> + /// Creates a new InternalException instance. + /// </summary> + public InternalException() : base() + { + } + + /// <summary> + /// Creates a new InternalException instance. + /// </summary> + public InternalException(string message) : base(message) + { + } + + /// <summary> + /// Creates a new InternalException instance. + /// </summary> + public InternalException(string message, Exception innerException) : base(message, innerException) + { + } + + /// <summary> + /// Creates a new InternalException instance. + /// </summary> + protected InternalException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/MergedCollection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/MergedCollection.cs new file mode 100644 index 000000000..79574053b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/MergedCollection.cs @@ -0,0 +1,70 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Two collections in sequence + /// </summary> + public class MergedCollection<T, TCollection> : ObservableCollection<T> where TCollection : INotifyCollectionChanged, IList<T> + { + TCollection a; + TCollection b; + + /// <summary> Create a wrapper containing elements of 'a' and then 'b' </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] + public MergedCollection(TCollection a, TCollection b) + { + this.a = a; + this.b = b; + + this.a.CollectionChanged += SourceCollectionAChanged; + this.b.CollectionChanged += SourceCollectionBChanged; + + Reset(); + } + + void Reset() + { + this.Clear(); + foreach(T item in a) this.Add(item); + foreach(T item in b) this.Add(item); + } + + void SourceCollectionAChanged(object sender, NotifyCollectionChangedEventArgs e) + { + SourceCollectionChanged(0, e); + } + + void SourceCollectionBChanged(object sender, NotifyCollectionChangedEventArgs e) + { + SourceCollectionChanged(a.Count, e); + } + + void SourceCollectionChanged(int collectionStart, NotifyCollectionChangedEventArgs e) + { + switch(e.Action) { + case NotifyCollectionChangedAction.Add: + for (int i = 0; i < e.NewItems.Count; i++) { + this.InsertItem(collectionStart + e.NewStartingIndex + i, (T)e.NewItems[i]); + } + break; + case NotifyCollectionChangedAction.Remove: + for (int i = 0; i < e.OldItems.Count; i++) { + this.RemoveAt(collectionStart + e.OldStartingIndex); + } + break; + case NotifyCollectionChangedAction.Reset: + Reset(); + break; + default: + throw new NotSupportedException(e.Action.ToString()); + } + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/PrettyPrintAXmlVisitor.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/PrettyPrintAXmlVisitor.cs new file mode 100644 index 000000000..cb393c552 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/PrettyPrintAXmlVisitor.cs @@ -0,0 +1,69 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Text; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Converts the XML tree back to text. + /// The text should exactly match the original. + /// </summary> + public class PrettyPrintAXmlVisitor: AbstractAXmlVisitor + { + StringBuilder sb = new StringBuilder(); + + /// <summary> + /// Gets the pretty printed text + /// </summary> + public string Output { + get { + return sb.ToString(); + } + } + + /// <summary> Create XML text from a document </summary> + public static string PrettyPrint(AXmlDocument doc) + { + PrettyPrintAXmlVisitor visitor = new PrettyPrintAXmlVisitor(); + visitor.VisitDocument(doc); + return visitor.Output; + } + + /// <summary> Visit RawDocument </summary> + public override void VisitDocument(AXmlDocument document) + { + base.VisitDocument(document); + } + + /// <summary> Visit RawElement </summary> + public override void VisitElement(AXmlElement element) + { + base.VisitElement(element); + } + + /// <summary> Visit RawTag </summary> + public override void VisitTag(AXmlTag tag) + { + sb.Append(tag.OpeningBracket); + sb.Append(tag.Name); + base.VisitTag(tag); + sb.Append(tag.ClosingBracket); + } + + /// <summary> Visit RawAttribute </summary> + public override void VisitAttribute(AXmlAttribute attribute) + { + sb.Append(attribute.Name); + sb.Append(attribute.EqualsSign); + sb.Append(attribute.QuotedValue); + } + + /// <summary> Visit RawText </summary> + public override void VisitText(AXmlText text) + { + sb.Append(text.EscapedValue); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/SyntaxError.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/SyntaxError.cs new file mode 100644 index 000000000..4c2e5d636 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/SyntaxError.cs @@ -0,0 +1,36 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> Information about syntax error that occured during parsing </summary> + public class SyntaxError: TextSegment + { + /// <summary> Object for which the error occured </summary> + public AXmlObject Object { get; internal set; } + /// <summary> Textual description of the error </summary> + public string Message { get; internal set; } + /// <summary> Any user data </summary> + public object Tag { get; set; } + + internal SyntaxError Clone(AXmlObject newOwner) + { + return new SyntaxError { + Object = newOwner, + Message = Message, + Tag = Tag, + StartOffset = StartOffset, + EndOffset = EndOffset, + }; + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagMatchingHeuristics.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagMatchingHeuristics.cs new file mode 100644 index 000000000..52c692cd8 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagMatchingHeuristics.cs @@ -0,0 +1,439 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Tango.Scripting.Editors.Utils; + +namespace Tango.Scripting.Editors.Xml +{ + class TagMatchingHeuristics + { + const int maxConfigurationCount = 10; + + AXmlParser parser; + TrackedSegmentCollection trackedSegments; + string input; + List<AXmlObject> tags; + + public TagMatchingHeuristics(AXmlParser parser, string input, List<AXmlObject> tags) + { + this.parser = parser; + this.trackedSegments = parser.TrackedSegments; + this.input = input; + this.tags = tags; + } + + public AXmlDocument ReadDocument() + { + AXmlDocument doc = new AXmlDocument() { Parser = parser }; + + // AXmlParser.Log("Flat stream: {0}", PrintObjects(tags)); + List<AXmlObject> valid = MatchTags(tags); + // AXmlParser.Log("Fixed stream: {0}", PrintObjects(valid)); + IEnumerator<AXmlObject> validStream = valid.GetEnumerator(); + validStream.MoveNext(); // Move to first + while(true) { + // End of stream? + try { + if (validStream.Current == null) break; + } catch (InvalidCastException) { + break; + } + doc.AddChild(ReadTextOrElement(validStream)); + } + + if (doc.Children.Count > 0) { + doc.StartOffset = doc.FirstChild.StartOffset; + doc.EndOffset = doc.LastChild.EndOffset; + } + + // Check well formed + foreach(AXmlTag xmlDeclaration in doc.Children.OfType<AXmlTag>().Where(t => t.IsProcessingInstruction && string.Equals(t.Name, "xml", StringComparison.OrdinalIgnoreCase))) { + if (xmlDeclaration.StartOffset != 0) + TagReader.OnSyntaxError(doc, xmlDeclaration.StartOffset, xmlDeclaration.StartOffset + 5, + "XML declaration must be at the start of document"); + } + int elemCount = doc.Children.OfType<AXmlElement>().Count(); + if (elemCount == 0) + TagReader.OnSyntaxError(doc, doc.EndOffset, doc.EndOffset, + "Root element is missing"); + if (elemCount > 1) { + AXmlElement next = doc.Children.OfType<AXmlElement>().Skip(1).First(); + TagReader.OnSyntaxError(doc, next.StartOffset, next.StartOffset, + "Only one root element is allowed"); + } + foreach(AXmlTag tag in doc.Children.OfType<AXmlTag>()) { + if (tag.IsCData) + TagReader.OnSyntaxError(doc, tag.StartOffset, tag.EndOffset, + "CDATA not allowed in document root"); + } + foreach(AXmlText text in doc.Children.OfType<AXmlText>()) { + if (!text.ContainsOnlyWhitespace) + TagReader.OnSyntaxError(doc, text.StartOffset, text.EndOffset, + "Only whitespace is allowed in document root"); + } + + + AXmlParser.Log("Constructed {0}", doc); + trackedSegments.AddParsedObject(doc, null); + return doc; + } + + static AXmlObject ReadSingleObject(IEnumerator<AXmlObject> objStream) + { + AXmlObject obj = objStream.Current; + objStream.MoveNext(); + return obj; + } + + AXmlObject ReadTextOrElement(IEnumerator<AXmlObject> objStream) + { + AXmlObject curr = objStream.Current; + if (curr is AXmlText || curr is AXmlElement) { + return ReadSingleObject(objStream); + } else { + AXmlTag currTag = (AXmlTag)curr; + if (currTag == StartTagPlaceholder) { + return ReadElement(objStream); + } else if (currTag.IsStartOrEmptyTag) { + return ReadElement(objStream); + } else { + return ReadSingleObject(objStream); + } + } + } + + AXmlElement ReadElement(IEnumerator<AXmlObject> objStream) + { + AXmlElement element = new AXmlElement(); + element.IsProperlyNested = true; + + // Read start tag + AXmlTag startTag = ReadSingleObject(objStream) as AXmlTag; + AXmlParser.DebugAssert(startTag != null, "Start tag expected"); + AXmlParser.DebugAssert(startTag.IsStartOrEmptyTag || startTag == StartTagPlaceholder, "Start tag expected"); + if (startTag == StartTagPlaceholder) { + element.HasStartOrEmptyTag = false; + element.IsProperlyNested = false; + TagReader.OnSyntaxError(element, objStream.Current.StartOffset, objStream.Current.EndOffset, + "Matching openning tag was not found"); + } else { + element.HasStartOrEmptyTag = true; + element.AddChild(startTag); + } + + // Read content and end tag + if (startTag == StartTagPlaceholder || // Check first in case the start tag is null + element.StartTag.IsStartTag) + { + while(true) { + AXmlTag currTag = objStream.Current as AXmlTag; // Peek + if (currTag == EndTagPlaceholder) { + TagReader.OnSyntaxError(element, element.LastChild.EndOffset, element.LastChild.EndOffset, + "Expected '</{0}>'", element.StartTag.Name); + ReadSingleObject(objStream); + element.HasEndTag = false; + element.IsProperlyNested = false; + break; + } else if (currTag != null && currTag.IsEndTag) { + if (element.HasStartOrEmptyTag && currTag.Name != element.StartTag.Name) { + TagReader.OnSyntaxError(element, currTag.StartOffset + 2, currTag.StartOffset + 2 + currTag.Name.Length, + "Expected '{0}'. End tag must have same name as start tag.", element.StartTag.Name); + } + element.AddChild(ReadSingleObject(objStream)); + element.HasEndTag = true; + break; + } + AXmlObject nested = ReadTextOrElement(objStream); + + AXmlElement nestedAsElement = nested as AXmlElement; + if (nestedAsElement != null) { + if (!nestedAsElement.IsProperlyNested) + element.IsProperlyNested = false; + element.AddChildren(Split(nestedAsElement).ToList()); + } else { + element.AddChild(nested); + } + } + } else { + element.HasEndTag = false; + } + + element.StartOffset = element.FirstChild.StartOffset; + element.EndOffset = element.LastChild.EndOffset; + + AXmlParser.Assert(element.HasStartOrEmptyTag || element.HasEndTag, "Must have at least start or end tag"); + + AXmlParser.Log("Constructed {0}", element); + trackedSegments.AddParsedObject(element, null); // Need all elements in cache for offset tracking + return element; + } + + IEnumerable<AXmlObject> Split(AXmlElement elem) + { + int myIndention = GetIndentLevel(elem); + // Has start tag and no end tag ? (other then empty-element tag) + if (elem.HasStartOrEmptyTag && elem.StartTag.IsStartTag && !elem.HasEndTag && myIndention != -1) { + int lastAccepted = 0; // Accept start tag + while (lastAccepted + 1 < elem.Children.Count) { + AXmlObject nextItem = elem.Children[lastAccepted + 1]; + if (nextItem is AXmlText) { + lastAccepted++; continue; // Accept + } else { + // Include all more indented items + if (GetIndentLevel(nextItem) > myIndention) { + lastAccepted++; continue; // Accept + } else { + break; // Reject + } + } + } + // Accepted everything? + if (lastAccepted + 1 == elem.Children.Count) { + yield return elem; + yield break; + } + AXmlParser.Log("Splitting {0} - take {1} of {2} nested", elem, lastAccepted, elem.Children.Count - 1); + AXmlElement topHalf = new AXmlElement(); + topHalf.HasStartOrEmptyTag = elem.HasStartOrEmptyTag; + topHalf.HasEndTag = elem.HasEndTag; + topHalf.AddChildren(elem.Children.Take(1 + lastAccepted)); // Start tag + nested + topHalf.StartOffset = topHalf.FirstChild.StartOffset; + topHalf.EndOffset = topHalf.LastChild.EndOffset; + TagReader.OnSyntaxError(topHalf, topHalf.LastChild.EndOffset, topHalf.LastChild.EndOffset, + "Expected '</{0}>'", topHalf.StartTag.Name); + + AXmlParser.Log("Constructed {0}", topHalf); + trackedSegments.AddParsedObject(topHalf, null); + yield return topHalf; + for(int i = lastAccepted + 1; i < elem.Children.Count; i++) { + yield return elem.Children[i]; + } + } else { + yield return elem; + } + } + + int GetIndentLevel(AXmlObject obj) + { + int offset = obj.StartOffset - 1; + int level = 0; + while(true) { + if (offset < 0) break; + char c = input[offset]; + if (c == ' ') { + level++; + } else if (c == '\t') { + level += 4; + } else if (c == '\r' || c == '\n') { + break; + } else { + return -1; + } + offset--; + } + return level; + } + + /// <summary> + /// Stack of still unmatched start tags. + /// It includes the cost and backtack information. + /// </summary> + class Configuration + { + /// <summary> Unmatched start tags </summary> + public ImmutableStack<AXmlTag> StartTags { get; set; } + /// <summary> Properly nested tags </summary> + public ImmutableStack<AXmlObject> Document { get; set; } + /// <summary> Number of needed modificaitons to the document </summary> + public int Cost { get; set; } + } + + /// <summary> + /// Dictionary which stores the cheapest configuration + /// </summary> + class Configurations: Dictionary<ImmutableStack<AXmlTag>, Configuration> + { + public Configurations() + { + } + + public Configurations(IEnumerable<Configuration> configs) + { + foreach(Configuration config in configs) { + this.Add(config); + } + } + + /// <summary> Overwrite only if cheaper </summary> + public void Add(Configuration newConfig) + { + Configuration oldConfig; + if (this.TryGetValue(newConfig.StartTags, out oldConfig)) { + if (newConfig.Cost < oldConfig.Cost) { + this[newConfig.StartTags] = newConfig; + } + } else { + base.Add(newConfig.StartTags, newConfig); + } + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + foreach(var kvp in this) { + sb.Append("\n - '"); + foreach(AXmlTag startTag in kvp.Value.StartTags.Reverse()) { + sb.Append('<'); + sb.Append(startTag.Name); + sb.Append('>'); + } + sb.AppendFormat("' = {0}", kvp.Value.Cost); + } + return sb.ToString(); + } + } + + // Tags used to guide the element creation + readonly AXmlTag StartTagPlaceholder = new AXmlTag(); + readonly AXmlTag EndTagPlaceholder = new AXmlTag(); + + /// <summary> + /// Add start or end tag placeholders so that the documment is properly nested + /// </summary> + List<AXmlObject> MatchTags(IEnumerable<AXmlObject> objs) + { + Configurations configurations = new Configurations(); + configurations.Add(new Configuration { + StartTags = ImmutableStack<AXmlTag>.Empty, + Document = ImmutableStack<AXmlObject>.Empty, + Cost = 0, + }); + foreach(AXmlObject obj in objs) { + configurations = ProcessObject(configurations, obj); + } + // Close any remaining start tags + foreach(Configuration conifg in configurations.Values) { + while(!conifg.StartTags.IsEmpty) { + conifg.StartTags = conifg.StartTags.Pop(); + conifg.Document = conifg.Document.Push(EndTagPlaceholder); + conifg.Cost += 1; + } + } + // AXmlParser.Log("Configurations after closing all remaining tags:" + configurations.ToString()); + Configuration bestConfig = configurations.Values.OrderBy(v => v.Cost).First(); + AXmlParser.Log("Best configuration has cost {0}", bestConfig.Cost); + + return bestConfig.Document.Reverse().ToList(); + } + + /// <summary> Get posible configurations after considering given object </summary> + Configurations ProcessObject(Configurations oldConfigs, AXmlObject obj) + { + AXmlParser.Log("Processing {0}", obj); + + AXmlTag objAsTag = obj as AXmlTag; + AXmlElement objAsElement = obj as AXmlElement; + AXmlParser.DebugAssert(objAsTag != null || objAsElement != null || obj is AXmlText, obj.GetType().Name + " not expected"); + if (objAsElement != null) + AXmlParser.Assert(objAsElement.IsProperlyNested, "Element not properly nested"); + + Configurations newConfigs = new Configurations(); + + foreach(var kvp in oldConfigs) { + Configuration oldConfig = kvp.Value; + var oldStartTags = oldConfig.StartTags; + var oldDocument = oldConfig.Document; + int oldCost = oldConfig.Cost; + + if (objAsTag != null && objAsTag.IsStartTag) { + newConfigs.Add(new Configuration { // Push start-tag (cost 0) + StartTags = oldStartTags.Push(objAsTag), + Document = oldDocument.Push(objAsTag), + Cost = oldCost, + }); + } else if (objAsTag != null && objAsTag.IsEndTag) { + newConfigs.Add(new Configuration { // Ignore (cost 1) + StartTags = oldStartTags, + Document = oldDocument.Push(StartTagPlaceholder).Push(objAsTag), + Cost = oldCost + 1, + }); + if (!oldStartTags.IsEmpty && oldStartTags.Peek().Name != objAsTag.Name) { + newConfigs.Add(new Configuration { // Pop 1 item (cost 1) - not mathcing + StartTags = oldStartTags.Pop(), + Document = oldDocument.Push(objAsTag), + Cost = oldCost + 1, + }); + } + int popedCount = 0; + var startTags = oldStartTags; + var doc = oldDocument; + foreach(AXmlTag poped in oldStartTags) { + popedCount++; + if (poped.Name == objAsTag.Name) { + newConfigs.Add(new Configuration { // Pop 'x' items (cost x-1) - last one is matching + StartTags = startTags.Pop(), + Document = doc.Push(objAsTag), + Cost = oldCost + popedCount - 1, + }); + } + startTags = startTags.Pop(); + doc = doc.Push(EndTagPlaceholder); + } + } else { + // Empty tag or other tag type or text or properly nested element + newConfigs.Add(new Configuration { // Ignore (cost 0) + StartTags = oldStartTags, + Document = oldDocument.Push(obj), + Cost = oldCost, + }); + } + } + + // Log("New configurations:" + newConfigs.ToString()); + + Configurations bestNewConfigurations = new Configurations( + newConfigs.Values.OrderBy(v => v.Cost).Take(maxConfigurationCount) + ); + + // AXmlParser.Log("Best new configurations:" + bestNewConfigurations.ToString()); + + return bestNewConfigurations; + } + + #region Helper methods + /* + string PrintObjects(IEnumerable<AXmlObject> objs) + { + StringBuilder sb = new StringBuilder(); + foreach(AXmlObject obj in objs) { + if (obj is AXmlTag) { + if (obj == StartTagPlaceholder) { + sb.Append("#StartTag#"); + } else if (obj == EndTagPlaceholder) { + sb.Append("#EndTag#"); + } else { + sb.Append(((AXmlTag)obj).OpeningBracket); + sb.Append(((AXmlTag)obj).Name); + sb.Append(((AXmlTag)obj).ClosingBracket); + } + } else if (obj is AXmlElement) { + sb.Append('['); + sb.Append(PrintObjects(((AXmlElement)obj).Children)); + sb.Append(']'); + } else if (obj is AXmlText) { + sb.Append('~'); + } else { + throw new InternalException("Should not be here: " + obj); + } + } + return sb.ToString(); + } + */ + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagReader.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagReader.cs new file mode 100644 index 000000000..1a1a34f6b --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagReader.cs @@ -0,0 +1,740 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Tango.Scripting.Editors.Xml +{ + class TagReader: TokenReader + { + AXmlParser parser; + TrackedSegmentCollection trackedSegments; + string input; + + public TagReader(AXmlParser parser, string input): base(input) + { + this.parser = parser; + this.trackedSegments = parser.TrackedSegments; + this.input = input; + } + + bool TryReadFromCacheOrNew<T>(out T res) where T: AXmlObject, new() + { + return TryReadFromCacheOrNew(out res, t => true); + } + + bool TryReadFromCacheOrNew<T>(out T res, Predicate<T> condition) where T: AXmlObject, new() + { + T cached = trackedSegments.GetCachedObject<T>(this.CurrentLocation, 0, condition); + if (cached != null) { + Skip(cached.Length); + AXmlParser.Assert(cached.Length > 0, "cached elements must not have zero length"); + res = cached; + return true; + } else { + res = new T(); + return false; + } + } + + void OnParsed(AXmlObject obj) + { + AXmlParser.Log("Parsed {0}", obj); + trackedSegments.AddParsedObject(obj, this.MaxTouchedLocation > this.CurrentLocation ? (int?)this.MaxTouchedLocation : null); + } + + /// <summary> + /// Read all tags in the document in a flat sequence. + /// It also includes the text between tags and possibly some properly nested Elements from cache. + /// </summary> + public List<AXmlObject> ReadAllTags() + { + List<AXmlObject> stream = new List<AXmlObject>(); + + while(true) { + if (IsEndOfFile()) { + break; + } else if (TryPeek('<')) { + AXmlElement elem; + if (TryReadFromCacheOrNew(out elem, e => e.IsProperlyNested)) { + stream.Add(elem); + } else { + stream.Add(ReadTag()); + } + } else { + stream.AddRange(ReadText(TextType.CharacterData)); + } + } + + return stream; + } + + /// <summary> + /// Context: "<" + /// </summary> + AXmlTag ReadTag() + { + AssertHasMoreData(); + + AXmlTag tag; + if (TryReadFromCacheOrNew(out tag)) return tag; + + tag.StartOffset = this.CurrentLocation; + + // Read the opening bracket + // It identifies the type of tag and parsing behavior for the rest of it + tag.OpeningBracket = ReadOpeningBracket(); + + if (tag.IsUnknownBang && !TryPeekWhiteSpace()) + OnSyntaxError(tag, tag.StartOffset, this.CurrentLocation, "Unknown tag"); + + if (tag.IsStartOrEmptyTag || tag.IsEndTag || tag.IsProcessingInstruction) { + // Read the name + string name; + if (TryReadName(out name)) { + if (!IsValidName(name)) { + OnSyntaxError(tag, this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name); + } + } else { + OnSyntaxError(tag, "Element name expected"); + } + tag.Name = name; + } else { + tag.Name = string.Empty; + } + + bool isXmlDeclr = tag.StartOffset == 0 && tag.Name == "xml"; + + if (tag.IsStartOrEmptyTag || tag.IsEndTag || isXmlDeclr) { + // Read attributes for the tag + while(true) { + // Chech for all forbiden 'name' charcters first - see ReadName + if (IsEndOfFile()) break; + if (TryPeekWhiteSpace()) { + tag.AddChildren(ReadText(TextType.WhiteSpace)); + continue; // End of file might be next + } + if (TryPeek('<')) break; + string endBr; + int endBrStart = this.CurrentLocation; // Just peek + if (TryReadClosingBracket(out endBr)) { // End tag + GoBack(endBrStart); + break; + } + + // We have "=\'\"" or name - read attribute + AXmlAttribute attr = ReadAttribulte(); + tag.AddChild(attr); + if (tag.IsEndTag) + OnSyntaxError(tag, attr.StartOffset, attr.EndOffset, "Attribute not allowed in end tag."); + } + } else if (tag.IsDocumentType) { + tag.AddChildren(ReadContentOfDTD()); + } else { + int start = this.CurrentLocation; + IEnumerable<AXmlObject> text; + if (tag.IsComment) { + text = ReadText(TextType.Comment); + } else if (tag.IsCData) { + text = ReadText(TextType.CData); + } else if (tag.IsProcessingInstruction) { + text = ReadText(TextType.ProcessingInstruction); + } else if (tag.IsUnknownBang) { + text = ReadText(TextType.UnknownBang); + } else { + throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket)); + } + // Enumerate + text = text.ToList(); + // Backtrack at complete start + if (IsEndOfFile() || (tag.IsUnknownBang && TryPeek('<'))) { + GoBack(start); + } else { + tag.AddChildren(text); + } + } + + // Read closing bracket + string bracket; + TryReadClosingBracket(out bracket); + tag.ClosingBracket = bracket; + + // Error check + int brStart = this.CurrentLocation - (tag.ClosingBracket ?? string.Empty).Length; + int brEnd = this.CurrentLocation; + if (tag.Name == null) { + // One error was reported already + } else if (tag.IsStartOrEmptyTag) { + if (tag.ClosingBracket != ">" && tag.ClosingBracket != "/>") OnSyntaxError(tag, brStart, brEnd, "'>' or '/>' expected"); + } else if (tag.IsEndTag) { + if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, brEnd, "'>' expected"); + } else if (tag.IsComment) { + if (tag.ClosingBracket != "-->") OnSyntaxError(tag, brStart, brEnd, "'-->' expected"); + } else if (tag.IsCData) { + if (tag.ClosingBracket != "]]>") OnSyntaxError(tag, brStart, brEnd, "']]>' expected"); + } else if (tag.IsProcessingInstruction) { + if (tag.ClosingBracket != "?>") OnSyntaxError(tag, brStart, brEnd, "'?>' expected"); + } else if (tag.IsUnknownBang) { + if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, brEnd, "'>' expected"); + } else if (tag.IsDocumentType) { + if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, brEnd, "'>' expected"); + } else { + throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket)); + } + + // Attribute name may not apper multiple times + var duplicates = tag.Children.OfType<AXmlAttribute>().GroupBy(attr => attr.Name).SelectMany(g => g.Skip(1)); + foreach(AXmlAttribute attr in duplicates) { + OnSyntaxError(tag, attr.StartOffset, attr.EndOffset, "Attribute with name '{0}' already exists", attr.Name); + } + + tag.EndOffset = this.CurrentLocation; + + OnParsed(tag); + return tag; + } + + /// <summary> + /// Reads any of the know opening brackets. (only full bracket) + /// Context: "<" + /// </summary> + string ReadOpeningBracket() + { + // We are using a lot of string literals so that the memory instances are shared + //int start = this.CurrentLocation; + if (TryRead('<')) { + if (TryRead('/')) { + return "</"; + } else if (TryRead('?')) { + return "<?"; + } else if (TryRead('!')) { + if (TryRead("--")) { + return "<!--"; + } else if (TryRead("[CDATA[")) { + return "<![CDATA["; + } else { + foreach(string dtdName in AXmlTag.DtdNames) { + // the dtdName includes "<!" + if (TryRead(dtdName.Remove(0, 2))) return dtdName; + } + return "<!"; + } + } else { + return "<"; + } + } else { + throw new InternalException("'<' expected"); + } + } + + /// <summary> + /// Reads any of the know closing brackets. (only full bracket) + /// Context: any + /// </summary> + bool TryReadClosingBracket(out string bracket) + { + // We are using a lot of string literals so that the memory instances are shared + if (TryRead('>')) { + bracket = ">"; + } else if (TryRead("/>")) { + bracket = "/>"; + } else if (TryRead("?>")) { + bracket = "?>"; + } else if (TryRead("-->")) { + bracket = "-->"; + } else if (TryRead("]]>")) { + bracket = "]]>"; + } else { + bracket = string.Empty; + return false; + } + return true; + } + + IEnumerable<AXmlObject> ReadContentOfDTD() + { + int start = this.CurrentLocation; + while(true) { + if (IsEndOfFile()) break; // End of file + TryMoveToNonWhiteSpace(); // Skip whitespace + if (TryRead('\'')) TryMoveTo('\''); // Skip single quoted string TODO: Bug + if (TryRead('\"')) TryMoveTo('\"'); // Skip single quoted string + if (TryRead('[')) { // Start of nested infoset + // Reading infoset + while(true) { + if (IsEndOfFile()) break; + TryMoveToAnyOf('<', ']'); + if (TryPeek('<')) { + if (start != this.CurrentLocation) { // Two following tags + yield return MakeText(start, this.CurrentLocation); + } + yield return ReadTag(); + start = this.CurrentLocation; + } + if (TryPeek(']')) break; + } + } + TryRead(']'); // End of nested infoset + if (TryPeek('>')) break; // Proper closing + if (TryPeek('<')) break; // Malformed XML + TryMoveNext(); // Skip anything else + } + if (start != this.CurrentLocation) { + yield return MakeText(start, this.CurrentLocation); + } + } + + /// <summary> + /// Context: name or "=\'\"" + /// </summary> + AXmlAttribute ReadAttribulte() + { + AssertHasMoreData(); + + AXmlAttribute attr; + if (TryReadFromCacheOrNew(out attr)) return attr; + + attr.StartOffset = this.CurrentLocation; + + // Read name + string name; + if (TryReadName(out name)) { + if (!IsValidName(name)) { + OnSyntaxError(attr, this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name); + } + } else { + OnSyntaxError(attr, "Attribute name expected"); + } + attr.Name = name; + + // Read equals sign and surrounding whitespace + int checkpoint = this.CurrentLocation; + TryMoveToNonWhiteSpace(); + if (TryRead('=')) { + int chk2 = this.CurrentLocation; + TryMoveToNonWhiteSpace(); + if (!TryPeek('"') && !TryPeek('\'')) { + // Do not read whitespace if quote does not follow + GoBack(chk2); + } + attr.EqualsSign = GetText(checkpoint, this.CurrentLocation); + } else { + GoBack(checkpoint); + OnSyntaxError(attr, "'=' expected"); + attr.EqualsSign = string.Empty; + } + + // Read attribute value + int start = this.CurrentLocation; + char quoteChar = TryPeek('"') ? '"' : '\''; + bool startsWithQuote; + if (TryRead(quoteChar)) { + startsWithQuote = true; + int valueStart = this.CurrentLocation; + TryMoveToAnyOf(quoteChar, '<'); + if (TryRead(quoteChar)) { + if (!TryPeekAnyOf(' ', '\t', '\n', '\r', '/', '>', '?')) { + if (TryPeekPrevious('=', 2) || (TryPeekPrevious('=', 3) && TryPeekPrevious(' ', 2))) { + // This actually most likely means that we are in the next attribute value + GoBack(valueStart); + ReadAttributeValue(quoteChar); + if (TryRead(quoteChar)) { + OnSyntaxError(attr, "White space or end of tag expected"); + } else { + OnSyntaxError(attr, "Quote {0} expected (or add whitespace after the following one)", quoteChar); + } + } else { + OnSyntaxError(attr, "White space or end of tag expected"); + } + } + } else { + // '<' or end of file + GoBack(valueStart); + ReadAttributeValue(quoteChar); + OnSyntaxError(attr, "Quote {0} expected", quoteChar); + } + } else { + startsWithQuote = false; + int valueStart = this.CurrentLocation; + ReadAttributeValue(null); + TryRead('\"'); + TryRead('\''); + if (valueStart == this.CurrentLocation) { + OnSyntaxError(attr, "Attribute value expected"); + } else { + OnSyntaxError(attr, valueStart, this.CurrentLocation, "Attribute value must be quoted"); + } + } + attr.QuotedValue = GetText(start, this.CurrentLocation); + attr.Value = Unquote(attr.QuotedValue); + attr.Value = Dereference(attr, attr.Value, startsWithQuote ? start + 1 : start); + + attr.EndOffset = this.CurrentLocation; + + OnParsed(attr); + return attr; + } + + /// <summary> + /// Read everything up to quote (excluding), opening/closing tag or attribute signature + /// </summary> + void ReadAttributeValue(char? quote) + { + while(true) { + if (IsEndOfFile()) return; + // What is next? + int start = this.CurrentLocation; + TryMoveToNonWhiteSpace(); // Read white space (if any) + if (quote.HasValue) { + if (TryPeek(quote.Value)) return; + } else { + if (TryPeek('"') || TryPeek('\'')) return; + } + // Opening/closing tag + string endBr; + if (TryPeek('<') || TryReadClosingBracket(out endBr)) { + GoBack(start); + return; + } + // Try reading attribute signature + string name; + if (TryReadName(out name)) { + int nameEnd = this.CurrentLocation; + if (TryMoveToNonWhiteSpace() && TryRead("=") && + TryMoveToNonWhiteSpace() && TryPeekAnyOf('"', '\'')) + { + // Start of attribute. Great + GoBack(start); + return; // Done + } else { + // Just some gargabe - make it part of the value + GoBack(nameEnd); + continue; // Read more + } + } + TryMoveNext(); // Accept everyting else + } + } + + AXmlText MakeText(int start, int end) + { + AXmlParser.DebugAssert(end > start, "Empty text"); + + AXmlText text = new AXmlText() { + StartOffset = start, + EndOffset = end, + EscapedValue = GetText(start, end), + Type = TextType.Other + }; + + OnParsed(text); + return text; + } + + const int maxEntityLength = 16; // The longest build-in one is 10 ("") + const int maxTextFragmentSize = 64; + const int lookAheadLength = (3 * maxTextFragmentSize) / 2; // More so that we do not get small "what was inserted" fragments + + /// <summary> + /// Reads text and optionaly separates it into fragments. + /// It can also return empty set for no appropriate text input. + /// Make sure you enumerate it only once + /// </summary> + IEnumerable<AXmlObject> ReadText(TextType type) + { + bool lookahead = false; + while(true) { + AXmlText text; + if (TryReadFromCacheOrNew(out text, t => t.Type == type)) { + // Cached text found + yield return text; + continue; // Read next fragment; the method can handle "no text left" + } + text.Type = type; + + // Limit the reading to just a few characters + // (the first character not to be read) + int fragmentEnd = Math.Min(this.CurrentLocation + maxTextFragmentSize, this.InputLength); + + // Look if some futher text has been already processed and align so that + // we hit that chache point. It is expensive so it is off for the first run + if (lookahead) { + // Note: Must fit entity + AXmlObject nextFragment = trackedSegments.GetCachedObject<AXmlText>(this.CurrentLocation + maxEntityLength, lookAheadLength - maxEntityLength, t => t.Type == type); + if (nextFragment != null) { + fragmentEnd = Math.Min(nextFragment.StartOffset, this.InputLength); + AXmlParser.Log("Parsing only text ({0}-{1}) because later text was already processed", this.CurrentLocation, fragmentEnd); + } + } + lookahead = true; + + text.StartOffset = this.CurrentLocation; + int start = this.CurrentLocation; + + // Whitespace would be skipped anyway by any operation + TryMoveToNonWhiteSpace(fragmentEnd); + int wsEnd = this.CurrentLocation; + + // Try move to the terminator given by the context + if (type == TextType.WhiteSpace) { + TryMoveToNonWhiteSpace(fragmentEnd); + } else if (type == TextType.CharacterData) { + while(true) { + if (!TryMoveToAnyOf(new char[] {'<', ']'}, fragmentEnd)) break; // End of fragment + if (TryPeek('<')) break; + if (TryPeek(']')) { + if (TryPeek("]]>")) { + OnSyntaxError(text, this.CurrentLocation, this.CurrentLocation + 3, "']]>' is not allowed in text"); + } + TryMoveNext(); + continue; + } + throw new Exception("Infinite loop"); + } + } else if (type == TextType.Comment) { + // Do not report too many errors + bool errorReported = false; + while(true) { + if (!TryMoveTo('-', fragmentEnd)) break; // End of fragment + if (TryPeek("-->")) break; + if (TryPeek("--") && !errorReported) { + OnSyntaxError(text, this.CurrentLocation, this.CurrentLocation + 2, "'--' is not allowed in comment"); + errorReported = true; + } + TryMoveNext(); + } + } else if (type == TextType.CData) { + while(true) { + // We can not use use TryMoveTo("]]>", fragmentEnd) because it may incorectly accept "]" at the end of fragment + if (!TryMoveTo(']', fragmentEnd)) break; // End of fragment + if (TryPeek("]]>")) break; + TryMoveNext(); + } + } else if (type == TextType.ProcessingInstruction) { + while(true) { + if (!TryMoveTo('?', fragmentEnd)) break; // End of fragment + if (TryPeek("?>")) break; + TryMoveNext(); + } + } else if (type == TextType.UnknownBang) { + TryMoveToAnyOf(new char[] {'<', '>'}, fragmentEnd); + } else { + throw new Exception("Uknown type " + type); + } + + text.ContainsOnlyWhitespace = (wsEnd == this.CurrentLocation); + + // Terminal found or real end was reached; + bool finished = this.CurrentLocation < fragmentEnd || IsEndOfFile(); + + if (!finished) { + // We have to continue reading more text fragments + + // If there is entity reference, make sure the next segment starts with it to prevent framentation + int entitySearchStart = Math.Max(start + 1 /* data for us */, this.CurrentLocation - maxEntityLength); + int entitySearchLength = this.CurrentLocation - entitySearchStart; + if (entitySearchLength > 0) { + // Note that LastIndexOf works backward + int entityIndex = input.LastIndexOf('&', this.CurrentLocation - 1, entitySearchLength); + if (entityIndex != -1) { + GoBack(entityIndex); + } + } + } + + text.EscapedValue = GetText(start, this.CurrentLocation); + if (type == TextType.CharacterData) { + // Normalize end of line first + text.Value = Dereference(text, NormalizeEndOfLine(text.EscapedValue), start); + } else { + text.Value = text.EscapedValue; + } + text.EndOffset = this.CurrentLocation; + + if (text.EscapedValue.Length > 0) { + OnParsed(text); + yield return text; + } + + if (finished) { + yield break; + } + } + } + + #region Helper methods + + void OnSyntaxError(AXmlObject obj, string message, params object[] args) + { + OnSyntaxError(obj, this.CurrentLocation, this.CurrentLocation + 1, message, args); + } + + public static void OnSyntaxError(AXmlObject obj, int start, int end, string message, params object[] args) + { + if (end <= start) end = start + 1; + string formattedMessage = string.Format(CultureInfo.InvariantCulture, message, args); + AXmlParser.Log("Syntax error ({0}-{1}): {2}", start, end, formattedMessage); + obj.AddSyntaxError(new SyntaxError() { + Object = obj, + StartOffset = start, + EndOffset = end, + Message = formattedMessage, + }); + } + + static bool IsValidName(string name) + { + try { + System.Xml.XmlConvert.VerifyName(name); + return true; + } catch (System.Xml.XmlException) { + return false; + } + } + + /// <summary> Remove quoting from the given string </summary> + static string Unquote(string quoted) + { + if (string.IsNullOrEmpty(quoted)) return string.Empty; + char first = quoted[0]; + if (quoted.Length == 1) return (first == '"' || first == '\'') ? string.Empty : quoted; + char last = quoted[quoted.Length - 1]; + if (first == '"' || first == '\'') { + if (first == last) { + // Remove both quotes + return quoted.Substring(1, quoted.Length - 2); + } else { + // Remove first quote + return quoted.Remove(0, 1); + } + } else { + if (last == '"' || last == '\'') { + // Remove last quote + return quoted.Substring(0, quoted.Length - 1); + } else { + // Keep whole string + return quoted; + } + } + } + + static string NormalizeEndOfLine(string text) + { + return text.Replace("\r\n", "\n").Replace("\r", "\n"); + } + + string Dereference(AXmlObject owner, string text, int textLocation) + { + StringBuilder sb = null; // The dereferenced text so far (all up to 'curr') + int curr = 0; + while(true) { + // Reached end of input + if (curr == text.Length) { + if (sb != null) { + return sb.ToString(); + } else { + return text; + } + } + + // Try to find reference + int start = text.IndexOf('&', curr); + + // No more references found + if (start == -1) { + if (sb != null) { + sb.Append(text, curr, text.Length - curr); // Add rest + return sb.ToString(); + } else { + return text; + } + } + + // Append text before the enitiy reference + if (sb == null) sb = new StringBuilder(text.Length); + sb.Append(text, curr, start - curr); + curr = start; + + // Process the entity + int errorLoc = textLocation + sb.Length; + + // Find entity name + int end = text.IndexOfAny(new char[] {'&', ';'}, start + 1, Math.Min(maxEntityLength, text.Length - (start + 1))); + if (end == -1 || text[end] == '&') { + // Not found + OnSyntaxError(owner, errorLoc, errorLoc + 1, "Entity reference must be terminated with ';'"); + // Keep '&' + sb.Append('&'); + curr++; + continue; // Restart and next character location + } + string name = text.Substring(start + 1, end - (start + 1)); + + // Resolve the name + string replacement; + if (name.Length == 0) { + replacement = null; + OnSyntaxError(owner, errorLoc + 1, errorLoc + 1, "Entity name expected"); + } else if (name == "amp") { + replacement = "&"; + } else if (name == "lt") { + replacement = "<"; + } else if (name == "gt") { + replacement = ">"; + } else if (name == "apos") { + replacement = "'"; + } else if (name == "quot") { + replacement = "\""; + } else if (name.Length > 0 && name[0] == '#') { + int num; + if (name.Length > 1 && name[1] == 'x') { + if (!int.TryParse(name.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture.NumberFormat, out num)) { + num = -1; + OnSyntaxError(owner, errorLoc + 3, errorLoc + 1 + name.Length, "Hexadecimal code of unicode character expected"); + } + } else { + if (!int.TryParse(name.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture.NumberFormat, out num)) { + num = -1; + OnSyntaxError(owner, errorLoc + 2, errorLoc + 1 + name.Length, "Numeric code of unicode character expected"); + } + } + if (num != -1) { + try { + replacement = char.ConvertFromUtf32(num); + } catch (ArgumentOutOfRangeException) { + replacement = null; + OnSyntaxError(owner, errorLoc + 2, errorLoc + 1 + name.Length, "Invalid unicode character U+{0:X} ({0})", num); + } + } else { + replacement = null; + } + } else if (!IsValidName(name)) { + replacement = null; + OnSyntaxError(owner, errorLoc + 1, errorLoc + 1, "Invalid entity name"); + } else { + replacement = null; + if (parser.UnknownEntityReferenceIsError) { + OnSyntaxError(owner, errorLoc, errorLoc + 1 + name.Length + 1, "Unknown entity reference '{0}'", name); + } + } + + // Append the replacement to output + if (replacement != null) { + sb.Append(replacement); + } else { + sb.Append('&'); + sb.Append(name); + sb.Append(';'); + } + curr = end + 1; + continue; + } + } + + #endregion + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TextType.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TextType.cs new file mode 100644 index 000000000..965185925 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TextType.cs @@ -0,0 +1,39 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> Identifies the context in which the text occured </summary> + enum TextType + { + /// <summary> Ends with non-whitespace </summary> + WhiteSpace, + + /// <summary> Ends with "<"; "]]>" is error </summary> + CharacterData, + + /// <summary> Ends with "-->"; "--" is error </summary> + Comment, + + /// <summary> Ends with "]]>" </summary> + CData, + + /// <summary> Ends with "?>" </summary> + ProcessingInstruction, + + /// <summary> Ends with "<" or ">" </summary> + UnknownBang, + + /// <summary> Unknown </summary> + Other + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TokenReader.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TokenReader.cs new file mode 100644 index 000000000..83d3315ed --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TokenReader.cs @@ -0,0 +1,309 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Tango.Scripting.Editors.Xml +{ + class TokenReader + { + string input; + int inputLength; + int currentLocation; + + // CurrentLocation is assumed to be touched and the fact does not + // have to be recorded in this variable. + // This stores any value bigger than that if applicable. + // Acutal value is max(currentLocation, maxTouchedLocation). + int maxTouchedLocation; + + public int InputLength { + get { return inputLength; } + } + + public int CurrentLocation { + get { return currentLocation; } + } + + public int MaxTouchedLocation { + get { return Math.Max(currentLocation, maxTouchedLocation); } + } + + public TokenReader(string input) + { + this.input = input; + this.inputLength = input.Length; + } + + protected bool IsEndOfFile() + { + return currentLocation == inputLength; + } + + protected bool HasMoreData() + { + return currentLocation < inputLength; + } + + protected void AssertHasMoreData() + { + AXmlParser.Assert(HasMoreData(), "Unexpected end of file"); + } + + protected bool TryMoveNext() + { + if (currentLocation == inputLength) return false; + + currentLocation++; + return true; + } + + protected void Skip(int count) + { + AXmlParser.Assert(currentLocation + count <= inputLength, "Skipping after the end of file"); + currentLocation += count; + } + + protected void GoBack(int oldLocation) + { + AXmlParser.Assert(oldLocation <= currentLocation, "Trying to move forward"); + maxTouchedLocation = Math.Max(maxTouchedLocation, currentLocation); + currentLocation = oldLocation; + } + + protected bool TryRead(char c) + { + if (currentLocation == inputLength) return false; + + if (input[currentLocation] == c) { + currentLocation++; + return true; + } else { + return false; + } + } + + protected bool TryReadAnyOf(params char[] c) + { + if (currentLocation == inputLength) return false; + + if (c.Contains(input[currentLocation])) { + currentLocation++; + return true; + } else { + return false; + } + } + + protected bool TryRead(string text) + { + if (TryPeek(text)) { + currentLocation += text.Length; + return true; + } else { + return false; + } + } + + protected bool TryPeekPrevious(char c, int back) + { + if (currentLocation - back == inputLength) return false; + if (currentLocation - back < 0 ) return false; + + return input[currentLocation - back] == c; + } + + protected bool TryPeek(char c) + { + if (currentLocation == inputLength) return false; + + return input[currentLocation] == c; + } + + protected bool TryPeekAnyOf(params char[] chars) + { + if (currentLocation == inputLength) return false; + + return chars.Contains(input[currentLocation]); + } + + protected bool TryPeek(string text) + { + if (!TryPeek(text[0])) return false; // Early exit + + maxTouchedLocation = Math.Max(maxTouchedLocation, currentLocation + (text.Length - 1)); + // The following comparison 'touches' the end of file - it does depend on the end being there + if (currentLocation + text.Length > inputLength) return false; + + return input.Substring(currentLocation, text.Length) == text; + } + + protected bool TryPeekWhiteSpace() + { + if (currentLocation == inputLength) return false; + + char c = input[currentLocation]; + return ((int)c <= 0x20) && (c == ' ' || c == '\t' || c == '\n' || c == '\r'); + } + + // The move functions do not have to move if already at target + // The move functions allow 'overriding' of the document length + + protected bool TryMoveTo(char c) + { + return TryMoveTo(c, inputLength); + } + + protected bool TryMoveTo(char c, int inputLength) + { + if (currentLocation == inputLength) return false; + int index = input.IndexOf(c, currentLocation, inputLength - currentLocation); + if (index != -1) { + currentLocation = index; + return true; + } else { + currentLocation = inputLength; + return false; + } + } + + protected bool TryMoveToAnyOf(params char[] c) + { + return TryMoveToAnyOf(c, inputLength); + } + + protected bool TryMoveToAnyOf(char[] c, int inputLength) + { + if (currentLocation == inputLength) return false; + int index = input.IndexOfAny(c, currentLocation, inputLength - currentLocation); + if (index != -1) { + currentLocation = index; + return true; + } else { + currentLocation = inputLength; + return false; + } + } + + protected bool TryMoveTo(string text) + { + return TryMoveTo(text, inputLength); + } + + protected bool TryMoveTo(string text, int inputLength) + { + if (currentLocation == inputLength) return false; + int index = input.IndexOf(text, currentLocation, inputLength - currentLocation, StringComparison.Ordinal); + if (index != -1) { + maxTouchedLocation = index + text.Length - 1; + currentLocation = index; + return true; + } else { + currentLocation = inputLength; + return false; + } + } + + protected bool TryMoveToNonWhiteSpace() + { + return TryMoveToNonWhiteSpace(inputLength); + } + + protected bool TryMoveToNonWhiteSpace(int inputLength) + { + while(true) { + if (currentLocation == inputLength) return false; // Reject end of file + char c = input[currentLocation]; + if (((int)c <= 0x20) && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) { + currentLocation++; // Accept white-space + continue; + } else { + return true; // Found non-white-space + } + } + } + + /// <summary> + /// Read a name token. + /// The following characters are not allowed: + /// "" End of file + /// " \n\r\t" Whitesapce + /// "=\'\"" Attribute value + /// "<>/?" Tags + /// </summary> + /// <returns> True if read at least one character </returns> + protected bool TryReadName(out string res) + { + int start = currentLocation; + // Keep reading up to invalid character + while(true) { + if (currentLocation == inputLength) break; // Reject end of file + char c = input[currentLocation]; + if (0x41 <= (int)c) { // Accpet from 'A' onwards + currentLocation++; + continue; + } + if (c == ' ' || c == '\n' || c == '\r' || c == '\t' || // Reject whitesapce + c == '=' || c == '\'' || c == '"' || // Reject attributes + c == '<' || c == '>' || c == '/' || c == '?') { // Reject tags + break; + } else { + currentLocation++; + continue; // Accept other character + } + } + if (start == currentLocation) { + res = string.Empty; + return false; + } else { + res = GetText(start, currentLocation); + return true; + } + } + + protected string GetText(int start, int end) + { + AXmlParser.Assert(end <= currentLocation, "Reading ahead of current location"); + if (start == inputLength && end == inputLength) { + return string.Empty; + } else { + return GetCachedString(input.Substring(start, end - start)); + } + } + + Dictionary<string, string> stringCache = new Dictionary<string, string>(); + int stringCacheRequestedCount; + int stringCacheRequestedSize; + int stringCacheStoredCount; + int stringCacheStoredSize; + + string GetCachedString(string cached) + { + stringCacheRequestedCount += 1; + stringCacheRequestedSize += 8 + 2 * cached.Length; + // Do not bother with long strings + if (cached.Length > 32) { + stringCacheStoredCount += 1; + stringCacheStoredSize += 8 + 2 * cached.Length; + return cached; + } + if (stringCache.ContainsKey(cached)) { + // Get the instance from the cache instead + return stringCache[cached]; + } else { + // Add to cache + stringCacheStoredCount += 1; + stringCacheStoredSize += 8 + 2 * cached.Length; + stringCache.Add(cached, cached); + return cached; + } + } + + public void PrintStringCacheStats() + { + AXmlParser.Log("String cache: Requested {0} ({1} bytes); Actaully stored {2} ({3} bytes); {4}% stored", stringCacheRequestedCount, stringCacheRequestedSize, stringCacheStoredCount, stringCacheStoredSize, stringCacheRequestedSize == 0 ? 0 : stringCacheStoredSize * 100 / stringCacheRequestedSize); + } + } +} diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TrackedSegmentCollection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TrackedSegmentCollection.cs new file mode 100644 index 000000000..f8e516591 --- /dev/null +++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TrackedSegmentCollection.cs @@ -0,0 +1,165 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +using Tango.Scripting.Editors.Document; + +namespace Tango.Scripting.Editors.Xml +{ + /// <summary> + /// Holds all objects that need to keep offsets up to date. + /// </summary> + class TrackedSegmentCollection + { + /// <summary> + /// Holds all types of objects in one collection. + /// </summary> + TextSegmentCollection<TextSegment> segments = new TextSegmentCollection<TextSegment>(); + + /// <summary> + /// Is used to identify what memory range was touched by object + /// The default is (StartOffset, EndOffset + 1) which is not stored + /// </summary> + class TouchedRange: TextSegment + { + public AXmlObject TouchedByObject { get; set; } + } + + public void UpdateOffsetsAndInvalidate(IEnumerable<DocumentChangeEventArgs> changes) + { + foreach(DocumentChangeEventArgs change in changes) { + // Update offsets of all items + segments.UpdateOffsets(change); + + // Remove any items affected by the change + AXmlParser.Log("Changed {0}-{1}", change.Offset, change.Offset + change.InsertionLength); + // Removing will cause one of the ends to be set to change.Offset + // FindSegmentsContaining includes any segments touching + // so that conviniently takes care of the +1 byte + var segmentsContainingOffset = segments.FindOverlappingSegments(change.Offset, change.InsertionLength); + foreach(AXmlObject obj in segmentsContainingOffset.OfType<AXmlObject>().Where(o => o.IsCached)) { + InvalidateCache(obj, false); + } + foreach(TouchedRange range in segmentsContainingOffset.OfType<TouchedRange>()) { + AXmlParser.Log("Found that {0} dependeds on ({1}-{2})", range.TouchedByObject, range.StartOffset, range.EndOffset); + InvalidateCache(range.TouchedByObject, true); + segments.Remove(range); + } + } + } + + /// <summary> + /// Invlidates all objects. That is, the whole document has changed. + /// </summary> + /// <remarks> We still have to keep the items becuase they might be in the document </remarks> + public void InvalidateAll() + { + AXmlParser.Log("Invalidating all objects"); + foreach(AXmlObject obj in segments.OfType<AXmlObject>()) { + obj.IsCached = false; + } + } + + /// <summary> Add object to cache, optionally adding extra memory tracking </summary> + public void AddParsedObject(AXmlObject obj, int? maxTouchedLocation) + { + if (!(obj.Length > 0 || obj is AXmlDocument)) + AXmlParser.Assert(false, string.Format(CultureInfo.InvariantCulture, "Invalid object {0}. It has zero length.", obj)); +// // Expensive check +// if (obj is AXmlContainer) { +// int objStartOffset = obj.StartOffset; +// int objEndOffset = obj.EndOffset; +// foreach(AXmlObject child in ((AXmlContainer)obj).Children) { +// AXmlParser.Assert(objStartOffset <= child.StartOffset && child.EndOffset <= objEndOffset, "Wrong nesting"); +// } +// } + segments.Add(obj); + AddSyntaxErrorsOf(obj); + obj.IsCached = true; + if (maxTouchedLocation != null) { + // location is assumed to be read so the range ends at (location + 1) + // For example eg for "a_" it is (0-2) + TouchedRange range = new TouchedRange() { + StartOffset = obj.StartOffset, + EndOffset = maxTouchedLocation.Value + 1, + TouchedByObject = obj + }; + segments.Add(range); + AXmlParser.Log("{0} touched range ({1}-{2})", obj, range.StartOffset, range.EndOffset); + } + } + + /// <summary> Removes object with all of its non-cached children </summary> + public void RemoveParsedObject(AXmlObject obj) + { + // Cached objects may be used in the future - do not remove them + if (obj.IsCached) return; + segments.Remove(obj); + RemoveSyntaxErrorsOf(obj); + AXmlParser.Log("Stopped tracking {0}", obj); + + AXmlContainer container = obj as AXmlContainer; + if (container != null) { + foreach (AXmlObject child in container.Children) { + RemoveParsedObject(child); + } + } + } + + public void AddSyntaxErrorsOf(AXmlObject obj) + { + foreach(SyntaxError syntaxError in obj.MySyntaxErrors) { + segments.Add(syntaxError); + } + } + + public void RemoveSyntaxErrorsOf(AXmlObject obj) + { + foreach(SyntaxError syntaxError in obj.MySyntaxErrors) { + segments.Remove(syntaxError); + } + } + + IEnumerable<AXmlObject> FindParents(AXmlObject child) + { + int childStartOffset = child.StartOffset; + int childEndOffset = child.EndOffset; + foreach(AXmlObject parent in segments.FindSegmentsContaining(child.StartOffset).OfType<AXmlObject>()) { + // Parent is anyone wholy containg the child + if (parent.StartOffset <= childStartOffset && childEndOffset <= parent.EndOffset && parent != child) { + yield return parent; + } + } + } + + /// <summary> Invalidates items, but keeps tracking them </summary> + /// <remarks> Can be called redundantly (from range tacking) </remarks> + void InvalidateCache(AXmlObject obj, bool includeParents) + { + if (includeParents) { + foreach(AXmlObject parent in FindParents(obj)) { + parent.IsCached = false; + AXmlParser.Log("Invalidating cached item {0} (it is parent)", parent); + } + } + obj.IsCached = false; + AXmlParser.Log("Invalidating cached item {0}", obj); + } + + public T GetCachedObject<T>(int offset, int lookaheadCount, Predicate<T> conditon) where T: AXmlObject, new() + { + TextSegment obj = segments.FindFirstSegmentWithStartAfter(offset); + while(obj != null && offset <= obj.StartOffset && obj.StartOffset <= offset + lookaheadCount) { + if (obj is T && ((AXmlObject)obj).IsCached && conditon((T)obj)) { + return (T)obj; + } + obj = segments.GetNextSegment(obj); + } + return null; + } + } +} |
