From fc8a05358a92cc3c77c5f1e30d536807ef0614fd Mon Sep 17 00:00:00 2001 From: Victoria Plitt Date: Mon, 8 Apr 2019 13:49:55 +0300 Subject: were added scripting projects --- .../Tango.Scripting.Editors/Xml/AXmlAttribute.cs | 129 ++++ .../Xml/AXmlAttributeCollection.cs | 119 ++++ .../Tango.Scripting.Editors/Xml/AXmlContainer.cs | 282 ++++++++ .../Tango.Scripting.Editors/Xml/AXmlDocument.cs | 69 ++ .../Tango.Scripting.Editors/Xml/AXmlElement.cs | 226 +++++++ .../Tango.Scripting.Editors/Xml/AXmlObject.cs | 266 ++++++++ .../Xml/AXmlObjectCollection.cs | 90 +++ .../Xml/AXmlObjectEventArgs.cs | 21 + .../Tango.Scripting.Editors/Xml/AXmlParser.cs | 201 ++++++ .../Tango.Scripting.Editors/Xml/AXmlTag.cs | 108 +++ .../Tango.Scripting.Editors/Xml/AXmlText.cs | 62 ++ .../Xml/AbstractAXmlVisitor.cs | 44 ++ .../Xml/CanonicalPrintAXmlVisitor.cs | 119 ++++ .../Xml/ExtensionMethods.cs | 47 ++ .../Xml/FilteredCollection.cs | 99 +++ .../Tango.Scripting.Editors/Xml/IAXmlVisitor.cs | 29 + .../Xml/InternalException.cs | 45 ++ .../Xml/MergedCollection.cs | 70 ++ .../Xml/PrettyPrintAXmlVisitor.cs | 69 ++ .../Tango.Scripting.Editors/Xml/SyntaxError.cs | 36 + .../Xml/TagMatchingHeuristics.cs | 439 ++++++++++++ .../Tango.Scripting.Editors/Xml/TagReader.cs | 740 +++++++++++++++++++++ .../Tango.Scripting.Editors/Xml/TextType.cs | 39 ++ .../Tango.Scripting.Editors/Xml/TokenReader.cs | 309 +++++++++ .../Xml/TrackedSegmentCollection.cs | 165 +++++ 25 files changed, 3823 insertions(+) create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttribute.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlAttributeCollection.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlContainer.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlDocument.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlElement.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObject.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectCollection.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlObjectEventArgs.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlParser.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlTag.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AXmlText.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/AbstractAXmlVisitor.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/CanonicalPrintAXmlVisitor.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/ExtensionMethods.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/FilteredCollection.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/IAXmlVisitor.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/InternalException.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/MergedCollection.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/PrettyPrintAXmlVisitor.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/SyntaxError.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagMatchingHeuristics.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TagReader.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TextType.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TokenReader.cs create mode 100644 Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml/TrackedSegmentCollection.cs (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Xml') 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 +{ + /// + /// Name-value pair in a tag + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public class AXmlAttribute: AXmlObject + { + /// Name with namespace prefix - exactly as in source file + public string Name { get; internal set; } + /// Equals sign and surrounding whitespace + public string EqualsSign { get; internal set; } + /// The raw value - exactly as in source file (*probably* quoted and escaped) + public string QuotedValue { get; internal set; } + /// Unquoted and dereferenced value of the attribute + 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 + + /// The element containing this attribute + /// Null if orphaned + public AXmlElement ParentElement { + get { + AXmlTag tag = this.Parent as AXmlTag; + if (tag != null) { + return tag.Parent as AXmlElement; + } + return null; + } + } + + /// The part of name before ":" + /// Empty string if not found + public string Prefix { + get { + return GetNamespacePrefix(this.Name); + } + } + + /// The part of name after ":" + /// Whole name if ":" not found + public string LocalName { + get { + return GetLocalName(this.Name); + } + } + + /// + /// 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." + /// + 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 + } + } + + /// Attribute is declaring namespace ("xmlns" or "xmlns:*") + public bool IsNamespaceDeclaration { + get { + return this.Name == "xmlns" || this.Prefix == "xmlns"; + } + } + + #endregion + + /// + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitAttribute(this); + } + + /// + 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; + } + } + + /// + 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 +{ + /// + /// Specailized attribute collection with attribute name caching + /// + public class AXmlAttributeCollection: FilteredCollection> + { + /// Empty unbound collection + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "InsertItem prevents modifying the Empty collection")] + public static readonly AXmlAttributeCollection Empty = new AXmlAttributeCollection(); + + /// Create unbound collection + protected AXmlAttributeCollection() {} + + /// Wrap the given collection. Non-attributes are filtered + public AXmlAttributeCollection(AXmlObjectCollection source): base(source) {} + + /// Wrap the given collection. Non-attributes are filtered. Items not matching the condition are filtered. + public AXmlAttributeCollection(AXmlObjectCollection source, Predicate condition): base(source, condition) {} + + Dictionary> hashtable = new Dictionary>(); + + void AddToHashtable(AXmlAttribute attr) + { + string localName = attr.LocalName; + if (!hashtable.ContainsKey(localName)) { + hashtable[localName] = new List(1); + } + hashtable[localName].Add(attr); + } + + void RemoveFromHashtable(AXmlAttribute attr) + { + string localName = attr.LocalName; + hashtable[localName].Remove(attr); + } + + static List NoAttributes = new List(); + + /// + /// Get all attributes with given local name. + /// Hash table is used for lookup so this is cheap. + /// + public IEnumerable GetByLocalName(string localName) + { + if (hashtable.ContainsKey(localName)) { + return hashtable[localName]; + } else { + return NoAttributes; + } + } + + /// + protected override void ClearItems() + { + foreach(AXmlAttribute item in this) { + RemoveFromHashtable(item); + item.Changing -= item_Changing; + item.Changed -= item_Changed; + } + base.ClearItems(); + } + + /// + 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); + } + + /// + protected override void RemoveItem(int index) + { + RemoveFromHashtable(this[index]); + this[index].Changing -= item_Changing; + this[index].Changed -= item_Changed; + base.RemoveItem(index); + } + + /// + 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 +{ + /// + /// Abstact base class for all types that can contain child nodes + /// + public abstract class AXmlContainer: AXmlObject + { + /// + /// Children of the node. It is read-only. + /// Note that is has CollectionChanged event. + /// + public AXmlObjectCollection Children { get; private set; } + + /// Create new container + protected AXmlContainer() + { + this.Children = new AXmlObjectCollection(); + } + + #region Helpper methods + + ObservableCollection elements; + + /// Gets direcly nested elements (non-recursive) + public ObservableCollection Elements { + get { + if (elements == null) { + elements = new FilteredCollection>(this.Children); + } + return elements; + } + } + + internal AXmlObject FirstChild { + get { + return this.Children[0]; + } + } + + internal AXmlObject LastChild { + get { + return this.Children[this.Children.Count - 1]; + } + } + + #endregion + + /// + public override IEnumerable GetSelfAndAllChildren() + { + return (new AXmlObject[] { this }).Flatten( + delegate(AXmlObject i) { + AXmlContainer container = i as AXmlContainer; + if (container != null) + return container.Children; + else + return null; + } + ); + } + + /// + /// Gets a child fully containg the given offset. + /// Goes recursively down the tree. + /// Specail case if at the end of attribute or text + /// + 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 + + /// To be used exlucively by the parser + 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); + } + + /// To be used exlucively by the parser + internal void AddChildren(IEnumerable 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()); + } + + /// + /// To be used exclusively by the children update algorithm. + /// Insert child and keep links consistent. + /// + 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); + } + + /// Recursively fix all parent pointer in a tree + /// + /// Cache constraint: + /// If cached item has parent set, then the whole subtree must be consistent and document set + /// + 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 + } + } + + /// + /// To be used exclusively by the children update algorithm. + /// Remove child, set parent to null and notify the document + /// + 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); + } + + /// Verify that the subtree is consistent. Only in debug build. + /// Parent pointers might be null or pointing somewhere else in parse tree + 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; + } + } + + /// + /// Note the the method is not called recuively. + /// Only the helper methods are recursive. + /// + internal void UpdateTreeFrom(AXmlContainer srcContainer) + { + this.StartOffset = srcContainer.StartOffset; // Force the update + this.UpdateDataFrom(srcContainer); + RemoveChildrenNotIn(srcContainer.Children); + InsertAndUpdateChildrenFrom(srcContainer.Children); + } + + void RemoveChildrenNotIn(IList srcList) + { + Dictionary 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 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 +{ + /// + /// The root object of the XML document + /// + public class AXmlDocument: AXmlContainer + { + /// Parser that produced this document + internal AXmlParser Parser { get; set; } + + /// Occurs when object is added to any part of the document + public event EventHandler ObjectInserted; + /// Occurs when object is removed from any part of the document + public event EventHandler ObjectRemoved; + /// Occurs before local data of any object in the document changes + public event EventHandler ObjectChanging; + /// Occurs after local data of any object in the document changed + public event EventHandler 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 } ); + } + + /// + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitDocument(this); + } + + /// + 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 +{ + /// + /// Logical grouping of other nodes together. + /// + public class AXmlElement: AXmlContainer + { + /// No tags are missing anywhere within this element (recursive) + public bool IsProperlyNested { get; set; } + /// True in wellformed XML + public bool HasStartOrEmptyTag { get; set; } + /// True in wellformed XML + public bool HasEndTag { get; set; } + + /// + 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; + } + } + + /// The start or empty-element tag if there is any + internal AXmlTag StartTag { + get { + Assert(HasStartOrEmptyTag, "Does not have a start tag"); + return (AXmlTag)this.Children[0]; + } + } + + /// The end tag if there is any + 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 + + /// Gets attributes of the element + /// + /// 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. + /// + public AXmlAttributeCollection Attributes { + get { + if (this.HasStartOrEmptyTag) { + return this.StartTag.Attributes; + } else { + return AXmlAttributeCollection.Empty; + } + } + } + + ObservableCollection attributesAndElements; + + /// Gets both attributes and elements. Expensive, avoid use. + /// Warning: the collection will regenerate after each update + public ObservableCollection AttributesAndElements { + get { + if (attributesAndElements == null) { + if (this.HasStartOrEmptyTag) { + attributesAndElements = new MergedCollection> ( + // New wrapper with RawObject types + new FilteredCollection>(this.StartTag.Children, x => x is AXmlAttribute), + new FilteredCollection>(this.Children, x => x is AXmlElement) + ); + } else { + attributesAndElements = new FilteredCollection>(this.Children, x => x is AXmlElement); + } + } + return attributesAndElements; + } + } + + /// Name with namespace prefix - exactly as in source + public string Name { + get { + if (this.HasStartOrEmptyTag) { + return this.StartTag.Name; + } else { + return this.EndTag.Name; + } + } + } + + /// The part of name before ":" + /// Empty string if not found + public string Prefix { + get { + return GetNamespacePrefix(this.Name); + } + } + + /// The part of name after ":" + /// Empty string if not found + public string LocalName { + get { + return GetLocalName(this.Name); + } + } + + /// Resolved namespace of the name + /// Empty string if prefix is not found + public string Namespace { + get { + string prefix = this.Prefix; + if (string.IsNullOrEmpty(prefix)) { + return FindDefaultNamespace(); + } else { + return ResolvePrefix(prefix); + } + } + } + + /// Find the defualt namespace for this context + 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 + } + + /// + /// Recursively resolve given prefix in this context. Prefix must have some value. + /// + /// Empty string if prefix is not found + 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 + } + + /// + /// Get unquoted value of attribute. + /// It looks in the no namespace (empty string). + /// + /// Null if not found + public string GetAttributeValue(string localName) + { + return GetAttributeValue(NoNamespace, localName); + } + + /// + /// Get unquoted value of attribute + /// + /// Namespace. Can be no namepace (empty string), which is the default for attributes. + /// Local name - text after ":" + /// Null if not found + 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 + + /// + public override void AcceptVisitor(IAXmlVisitor visitor) + { + visitor.VisitElement(this); + } + + /// + 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 +{ + /// + /// Abstact base class for all types + /// + public abstract class AXmlObject: TextSegment + { + /// Empty string. The namespace used if there is no "xmlns" specified + public static readonly string NoNamespace = string.Empty; + + /// Namespace for "xml:" prefix: "http://www.w3.org/XML/1998/namespace" + public static readonly string XmlNamespace = "http://www.w3.org/XML/1998/namespace"; + + /// Namesapce for "xmlns:" prefix: "http://www.w3.org/2000/xmlns/" + public static readonly string XmlnsNamespace = "http://www.w3.org/2000/xmlns/"; + + /// Parent node. + /// + /// New cached items start with null parent. + /// Cache constraint: + /// If cached item has parent set, then the whole subtree must be consistent + /// + public AXmlObject Parent { get; set; } + + /// + /// Gets the document that has owns this object. + /// Once set, it is not changed. Not even set to null. + /// + internal AXmlDocument Document { get; set; } + + /// Creates new object + protected AXmlObject() + { + this.LastUpdatedFrom = this; + } + + /// Occurs before the value of any local properties changes. Nested changes do not cause the event to occur + public event EventHandler Changing; + + /// Occurs after the value of any local properties changed. Nested changes do not cause the event to occur + public event EventHandler Changed; + + /// Raises Changing event + 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(); + } + } + + /// Raises Changed event + 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 syntaxErrors; + + /// + /// The error that occured in the context of this node (excluding nested nodes) + /// + public IEnumerable MySyntaxErrors { + get { + if (syntaxErrors == null) { + return new SyntaxError[] {}; + } else { + return syntaxErrors; + } + } + } + + /// + /// The error that occured in the context of this node and all nested nodes. + /// It has O(n) cost. + /// + public IEnumerable 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(); + syntaxErrors.Add(error); + } + + /// Throws exception if condition is false + /// Present in release mode - use only for very cheap aserts + protected static void Assert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + /// Throws exception if condition is false + [Conditional("DEBUG")] + protected static void DebugAssert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + /// Recursively gets self and all nested nodes. + [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 GetSelfAndAllChildren() + { + return new AXmlObject[] { this }; + } + + /// Get all ancestors of this node + [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 GetAncestors() + { + AXmlObject curr = this.Parent; + while(curr != null) { + yield return curr; + curr = curr.Parent; + } + } + + /// Call appropriate visit method on the given visitor + public abstract void AcceptVisitor(IAXmlVisitor visitor); + + /// The parser tree object this object was updated from + /// Initialized to 'this' + internal AXmlObject LastUpdatedFrom { get; private set; } + + internal bool IsCached { get; set; } + + /// Is call to UpdateDataFrom is allowed? + internal bool CanUpdateDataFrom(AXmlObject source) + { + return + this.GetType() == source.GetType() && + this.StartOffset == source.StartOffset && + (this.LastUpdatedFrom == source || !this.IsCached); + } + + /// Copy all data from the 'source' to this object + /// Returns true if any updates were done + 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(); + 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; + } + + /// Verify that the item is consistent. Only in debug build. + [Conditional("DEBUG")] + internal virtual void DebugCheckConsistency(bool allowNullParent) + { + + } + + /// + 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 + + /// The part of name before ":" + /// Empty string if not found + 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; + } + } + + /// The part of name after ":" + /// Whole name if ":" not found + 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 +{ + /// + /// Collection that is publicly read-only and has support + /// for adding/removing multiple items at a time. + /// + public class AXmlObjectCollection: Collection, INotifyCollectionChanged + { + /// Occurs when the collection is changed + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// Raises event + // Do not inherit - it is not called if event is null + void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (CollectionChanged != null) { + CollectionChanged(this, e); + } + } + + /// + protected override void ClearItems() + { + throw new NotSupportedException(); + } + + /// + protected override void InsertItem(int index, T item) + { + throw new NotSupportedException(); + } + + /// + protected override void RemoveItem(int index) + { + throw new NotSupportedException(); + } + + /// + 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 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 removed = new List(); + 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 +{ + /// Holds event args for event caused by + public class AXmlObjectEventArgs: EventArgs + { + /// The object that caused the event + 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 +{ + /// + /// Creates object tree from XML document. + /// + /// + /// 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: + /// + /// + /// Make everything as simple as possible, but not simpler. + /// + /// + /// 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 "" + /// ]]> + /// + /// The precise content of RawTag depends on what it represents: + /// " | "/>") + /// End tag: "" + /// P.instr.: "" + /// Comment: "" + /// CData: "" + /// DTD: "" (DOCTYPE or other DTD names) + /// UknownBang: "" + /// ]]> + /// + /// 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. + /// + /// + public class AXmlParser + { + AXmlDocument userDocument; + + internal TrackedSegmentCollection TrackedSegments { get; private set; } + + /// + /// Generate syntax error when seeing enity reference other then the build-in ones + /// + public bool UnknownEntityReferenceIsError { get; set; } + + /// Create new parser + public AXmlParser() + { + this.Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + ClearInternal(); + } + + /// Throws exception if condition is false + internal static void Assert(bool condition, string message) + { + if (!condition) { + throw new InternalException("Assertion failed: " + message); + } + } + + /// Throws exception if condition is false + [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)); + } + + /// + /// Incrementaly parse the given text. + /// You have to hold the write lock. + /// + /// + /// The full XML text of the new document. + /// + /// + /// Changes since last parse. Null will cause full reparse. + /// + public AXmlDocument Parse(string input, IEnumerable 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 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; + } + + /// + /// Makes calls to Parse() thread-safe. Use Lock everywhere Parse() is called. + /// + public ReaderWriterLockSlim Lock { get; private set; } + + /// + /// Returns the last cached version of the document. + /// + /// No read lock is held by the current thread. + public AXmlDocument LastDocument { + get { + if (!Lock.IsReadLockHeld) + throw new InvalidOperationException("Read lock needed!"); + + return userDocument; + } + } + + /// + /// Clears the parser data. + /// + /// No write lock is held by the current thread. + 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 +{ + /// + /// Represents any markup starting with "<" and (hopefully) ending with ">" + /// + public class AXmlTag: AXmlContainer + { + /// These identify the start of DTD elements + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification="ReadOnlyCollection is immutable")] + public static readonly ReadOnlyCollection DtdNames = new ReadOnlyCollection( + new string[] {" Opening bracket - usually "<" + public string OpeningBracket { get; internal set; } + /// Name following the opening bracket + public string Name { get; internal set; } + /// Opening bracket - usually ">" + public string ClosingBracket { get; internal set; } + + /// True if tag starts with "<" + public bool IsStartOrEmptyTag { get { return OpeningBracket == "<"; } } + /// True if tag starts with "<" and ends with ">" + public bool IsStartTag { get { return OpeningBracket == "<" && ClosingBracket == ">"; } } + /// True if tag starts with "<" and does not end with ">" + public bool IsEmptyTag { get { return OpeningBracket == "<" && ClosingBracket != ">" ; } } + /// True if tag starts with "</" + public bool IsEndTag { get { return OpeningBracket == " True if tag starts with "<?" + public bool IsProcessingInstruction { get { return OpeningBracket == " True if tag starts with "<!--" + public bool IsComment { get { return OpeningBracket == "") 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().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; + } + + /// + /// Reads any of the know opening brackets. (only full bracket) + /// Context: "<" + /// + 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 " + /// Reads any of the know closing brackets. (only full bracket) + /// Context: any + /// + 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 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); + } + } + + /// + /// Context: name or "=\'\"" + /// + 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; + } + + /// + /// Read everything up to quote (excluding), opening/closing tag or attribute signature + /// + 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 + + /// + /// 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 + /// + IEnumerable 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(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; + } + } + + /// Remove quoting from the given string + 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 +{ + /// Identifies the context in which the text occured + enum TextType + { + /// Ends with non-whitespace + WhiteSpace, + + /// Ends with "<"; "]]>" is error + CharacterData, + + /// Ends with "-->"; "--" is error + Comment, + + /// Ends with "]]>" + CData, + + /// Ends with "?>" + ProcessingInstruction, + + /// Ends with "<" or ">" + UnknownBang, + + /// Unknown + 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 + } + } + } + + /// + /// Read a name token. + /// The following characters are not allowed: + /// "" End of file + /// " \n\r\t" Whitesapce + /// "=\'\"" Attribute value + /// "<>/?" Tags + /// + /// True if read at least one character + 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 stringCache = new Dictionary(); + 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 +{ + /// + /// Holds all objects that need to keep offsets up to date. + /// + class TrackedSegmentCollection + { + /// + /// Holds all types of objects in one collection. + /// + TextSegmentCollection segments = new TextSegmentCollection(); + + /// + /// Is used to identify what memory range was touched by object + /// The default is (StartOffset, EndOffset + 1) which is not stored + /// + class TouchedRange: TextSegment + { + public AXmlObject TouchedByObject { get; set; } + } + + public void UpdateOffsetsAndInvalidate(IEnumerable 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().Where(o => o.IsCached)) { + InvalidateCache(obj, false); + } + foreach(TouchedRange range in segmentsContainingOffset.OfType()) { + AXmlParser.Log("Found that {0} dependeds on ({1}-{2})", range.TouchedByObject, range.StartOffset, range.EndOffset); + InvalidateCache(range.TouchedByObject, true); + segments.Remove(range); + } + } + } + + /// + /// Invlidates all objects. That is, the whole document has changed. + /// + /// We still have to keep the items becuase they might be in the document + public void InvalidateAll() + { + AXmlParser.Log("Invalidating all objects"); + foreach(AXmlObject obj in segments.OfType()) { + obj.IsCached = false; + } + } + + /// Add object to cache, optionally adding extra memory tracking + 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); + } + } + + /// Removes object with all of its non-cached children + 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 FindParents(AXmlObject child) + { + int childStartOffset = child.StartOffset; + int childEndOffset = child.EndOffset; + foreach(AXmlObject parent in segments.FindSegmentsContaining(child.StartOffset).OfType()) { + // Parent is anyone wholy containg the child + if (parent.StartOffset <= childStartOffset && childEndOffset <= parent.EndOffset && parent != child) { + yield return parent; + } + } + } + + /// Invalidates items, but keeps tracking them + /// Can be called redundantly (from range tacking) + 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(int offset, int lookaheadCount, Predicate 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; + } + } +} -- cgit v1.3.1