// 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
}
}