// 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 ICSharpCode.AvalonEdit.Document; namespace ICSharpCode.AvalonEdit.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 } }