Browse Source

XML Parser: Report syntax errors. Improved consistency of tree.

git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@4616 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61
shortcuts
David Srbecký 16 years ago
parent
commit
418093e238
  1. 267
      samples/XmlDOM/TextMarkerService.cs
  2. 5
      samples/XmlDOM/Window1.xaml
  3. 37
      samples/XmlDOM/Window1.xaml.cs
  4. 4
      samples/XmlDOM/XmlDOM.csproj
  5. 6
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegment.cs
  6. 207
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/XmlParser/RawObjects.cs
  7. 260
      src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/XmlParser/XmlParser.cs

267
samples/XmlDOM/TextMarkerService.cs

@ -0,0 +1,267 @@ @@ -0,0 +1,267 @@
// <file>
// <copyright see="prj:///doc/copyright.txt"/>
// <license see="prj:///doc/license.txt"/>
// <author name="Daniel Grunwald"/>
// <version>$Revision$</version>
// </file>
using ICSharpCode.AvalonEdit.Editing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Rendering;
namespace XmlDOM
{
/// <summary>
/// Handles the text markers for a code editor.
/// </summary>
sealed class TextMarkerService : DocumentColorizingTransformer, IBackgroundRenderer, ITextMarkerService
{
internal TextSegmentCollection<TextMarker> markers;
TextArea area;
public TextMarkerService(TextArea area)
{
this.area = area;
markers = new TextSegmentCollection<TextMarker>(area.Document);
this.area.TextView.BackgroundRenderers.Add(this);
this.area.TextView.LineTransformers.Add(this);
}
#region ITextMarkerService
public ITextMarker Create(int startOffset, int length)
{
TextMarker m = new TextMarker(this, startOffset, length);
markers.Add(m);
// no need to mark segment for redraw: the text marker is invisible until a property is set
return m;
}
public IEnumerable<ITextMarker> TextMarkers {
get { return markers.Cast<ITextMarker>(); }
}
public void RemoveAll(Predicate<ITextMarker> predicate)
{
if (predicate == null)
throw new ArgumentNullException("predicate");
foreach (TextMarker m in markers.ToArray()) {
if (predicate(m))
m.Delete();
}
}
internal void Remove(TextMarker marker)
{
markers.Remove(marker);
Redraw(marker);
}
/// <summary>
/// Redraws the specified text segment.
/// </summary>
internal void Redraw(ISegment segment)
{
area.TextView.Redraw(segment, DispatcherPriority.Normal);
}
#endregion
#region DocumentColorizingTransformer
protected override void ColorizeLine(DocumentLine line)
{
if (markers == null)
return;
int lineStart = line.Offset;
int lineEnd = lineStart + line.Length;
foreach (TextMarker marker in markers.FindOverlappingSegments(lineStart, line.Length)) {
Brush foregroundBrush = null;
if (marker.ForegroundColor != null) {
foregroundBrush = new SolidColorBrush(marker.ForegroundColor.Value);
foregroundBrush.Freeze();
}
ChangeLinePart(
Math.Max(marker.StartOffset, lineStart),
Math.Min(marker.EndOffset, lineEnd),
element => {
if (foregroundBrush != null) {
element.TextRunProperties.SetForegroundBrush(foregroundBrush);
}
}
);
}
}
#endregion
#region IBackgroundRenderer
public KnownLayer Layer {
get {
// draw behind selection
return KnownLayer.Selection;
}
}
public void Draw(TextView textView, DrawingContext drawingContext)
{
if (textView == null)
throw new ArgumentNullException("textView");
if (drawingContext == null)
throw new ArgumentNullException("drawingContext");
if (markers == null || !textView.VisualLinesValid)
return;
var visualLines = textView.VisualLines;
if (visualLines.Count == 0)
return;
int viewStart = visualLines.First().FirstDocumentLine.Offset;
int viewEnd = visualLines.Last().LastDocumentLine.Offset + visualLines.Last().LastDocumentLine.Length;
foreach (TextMarker marker in markers.FindOverlappingSegments(viewStart, viewEnd - viewStart)) {
if (marker.BackgroundColor != null) {
BackgroundGeometryBuilder geoBuilder = new BackgroundGeometryBuilder();
geoBuilder.AddSegment(textView, marker);
PathGeometry geometry = geoBuilder.CreateGeometry();
if (geometry != null) {
Color color = marker.BackgroundColor.Value;
SolidColorBrush brush = new SolidColorBrush(color);
brush.Freeze();
drawingContext.DrawGeometry(brush, null, geometry);
}
}
}
}
#endregion
}
sealed class TextMarker : TextSegment, ITextMarker
{
readonly TextMarkerService service;
public TextMarker(TextMarkerService service, int startOffset, int length)
{
if (service == null)
throw new ArgumentNullException("service");
this.service = service;
this.StartOffset = startOffset;
this.Length = length;
}
public event EventHandler Deleted;
public bool IsDeleted {
get { return !this.IsConnectedToCollection; }
}
public void Delete()
{
if (this.IsConnectedToCollection) {
service.Remove(this);
if (Deleted != null)
Deleted(this, EventArgs.Empty);
}
}
void Redraw()
{
service.Redraw(this);
}
Color? backgroundColor;
public Color? BackgroundColor {
get { return backgroundColor; }
set {
if (backgroundColor != value) {
backgroundColor = value;
Redraw();
}
}
}
Color? foregroundColor;
public Color? ForegroundColor {
get { return foregroundColor; }
set {
if (foregroundColor != value) {
foregroundColor = value;
Redraw();
}
}
}
public object Tag { get; set; }
}
/// <summary>
/// Represents a text marker.
/// </summary>
public interface ITextMarker
{
/// <summary>
/// Gets the start offset of the marked text region.
/// </summary>
int StartOffset { get; }
/// <summary>
/// Gets the end offset of the marked text region.
/// </summary>
int EndOffset { get; }
/// <summary>
/// Gets the length of the marked region.
/// </summary>
int Length { get; }
/// <summary>
/// Deletes the text marker.
/// </summary>
void Delete();
/// <summary>
/// Gets whether the text marker was deleted.
/// </summary>
bool IsDeleted { get; }
/// <summary>
/// Event that occurs when the text marker is deleted.
/// </summary>
event EventHandler Deleted;
/// <summary>
/// Gets/Sets the background color.
/// </summary>
Color? BackgroundColor { get; set; }
/// <summary>
/// Gets/Sets the foreground color.
/// </summary>
Color? ForegroundColor { get; set; }
/// <summary>
/// Gets/Sets an object with additional data for this text marker.
/// </summary>
object Tag { get; set; }
}
public interface ITextMarkerService
{
/// <summary>
/// Creates a new text marker. The text marker will be invisible at first,
/// you need to set one of the Color properties to make it visible.
/// </summary>
ITextMarker Create(int startOffset, int length);
/// <summary>
/// Gets the list of text markers.
/// </summary>
IEnumerable<ITextMarker> TextMarkers { get; }
/// <summary>
/// Removes all text markers that match the condition.
/// </summary>
void RemoveAll(Predicate<ITextMarker> predicate);
}
}

5
samples/XmlDOM/Window1.xaml

@ -31,7 +31,10 @@ @@ -31,7 +31,10 @@
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ic:TextEditor x:Name="editor"/>
<DockPanel Grid.Column="0">
<TextBlock DockPanel.Dock="Top" Name="errorText" Margin="2" Background="WhiteSmoke"/>
<ic:TextEditor x:Name="editor"/>
</DockPanel>
<DockPanel Grid.Column="1">
<Button DockPanel.Dock="Top" Content="Parse" Click="Button_Click"/>
<TreeView Name="treeView"/>

37
samples/XmlDOM/Window1.xaml.cs

@ -5,7 +5,9 @@ @@ -5,7 +5,9 @@
// <version>$Revision$</version>
// </file>
using ICSharpCode.AvalonEdit.Document;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Windows;
@ -16,7 +18,6 @@ using System.Windows.Input; @@ -16,7 +18,6 @@ using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using ICSharpCode.AvalonEdit.XmlParser;
namespace XmlDOM
@ -35,8 +36,14 @@ namespace XmlDOM @@ -35,8 +36,14 @@ namespace XmlDOM
InitializeComponent();
}
TextMarkerService markerService;
protected override void OnInitialized(EventArgs e)
{
markerService = new TextMarkerService(editor.TextArea);
editor.TextArea.TextView.MouseMove += new MouseEventHandler(editor_TextArea_TextView_MouseMove);
editor.Document.Changed += delegate { textDirty = true; };
parser = new XmlParser(editor.Document);
@ -47,6 +54,20 @@ namespace XmlDOM @@ -47,6 +54,20 @@ namespace XmlDOM
base.OnInitialized(e);
}
void editor_TextArea_TextView_MouseMove(object sender, MouseEventArgs e)
{
var pos = editor.TextArea.TextView.GetPosition(e.GetPosition(editor.TextArea.TextView));
if (pos.HasValue) {
int offset = editor.Document.GetOffset(new TextLocation(pos.Value.Line, pos.Value.Column));
var marker = markerService.markers.FindSegmentsContaining(offset).FirstOrDefault();
if (marker != null) {
errorText.Text = (string)marker.Tag;
} else {
errorText.Text = string.Empty;
}
}
}
void Button_Click(object sender, RoutedEventArgs e)
{
@ -59,16 +80,22 @@ namespace XmlDOM @@ -59,16 +80,22 @@ namespace XmlDOM
visitor.VisitDocument(doc);
string prettyPrintedText = visitor.Output;
if (prettyPrintedText != editor.Document.Text) {
MessageBox.Show("Original and pretty printer version of XML differ");
MessageBox.Show("Error - Original and pretty printed version of XML differ");
}
markerService.RemoveAll(m => true);
foreach(var error in parser.SyntaxErrors) {
var marker = markerService.Create(error.StartOffset, error.EndOffset - error.StartOffset);
marker.Tag = error.Message;
marker.BackgroundColor = Color.FromRgb(255, 150, 150);
}
textDirty = false;
}
void BindObject(object sender, EventArgs e)
{
TextBlock textBlock = (TextBlock)sender;
RawObject node = (RawObject)textBlock.DataContext;
node.LocalDataChanged += delegate {
node.Changed += delegate {
BindingOperations.GetBindingExpression(textBlock, TextBlock.TextProperty).UpdateTarget();
textBlock.Background = new SolidColorBrush(Colors.LightGreen);
Storyboard sb = ((Storyboard)this.FindResource("anim"));
@ -81,7 +108,7 @@ namespace XmlDOM @@ -81,7 +108,7 @@ namespace XmlDOM
{
TextBlock textBlock = (TextBlock)sender;
RawElement node = (RawElement)textBlock.DataContext;
node.StartTag.LocalDataChanged += delegate {
node.StartTag.Changed += delegate {
BindingOperations.GetBindingExpression(textBlock, TextBlock.TextProperty).UpdateTarget();
textBlock.Background = new SolidColorBrush(Colors.LightGreen);
Storyboard sb = ((Storyboard)this.FindResource("anim"));

4
samples/XmlDOM/XmlDOM.csproj

@ -34,6 +34,9 @@ @@ -34,6 +34,9 @@
<RequiredTargetFramework>3.0</RequiredTargetFramework>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase">
@ -50,6 +53,7 @@ @@ -50,6 +53,7 @@
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\WPFAssemblyInfo.cs" />
<Compile Include="TextMarkerService.cs" />
<Compile Include="Window1.xaml.cs">
<SubType>Code</SubType>
<DependentUpon>Window1.xaml</DependentUpon>

6
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/TextSegment.cs

@ -112,6 +112,12 @@ namespace ICSharpCode.AvalonEdit.Document @@ -112,6 +112,12 @@ namespace ICSharpCode.AvalonEdit.Document
get {
return StartOffset + Length;
}
set {
int newLength = value - StartOffset;
if (newLength < 0)
throw new ArgumentOutOfRangeException("value", "EndOffset must be greater or equal to StartOffset");
Length = newLength;
}
}
/// <summary>

207
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/XmlParser/RawObjects.cs

@ -35,8 +35,17 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -35,8 +35,17 @@ namespace ICSharpCode.AvalonEdit.XmlParser
/// </summary>
internal object ReadCallID { get; private set; }
/// <summary>
/// Parent node.
///
/// Some constraints:
/// - Reachable childs shall have parent pointer (except Document)
/// - Parser tree can reuse data of other trees as long as it does not modify them
/// (that, it can not set parent pointer if non-null)
/// </summary>
public RawObject Parent { get; set; }
// TODO: Performance
public RawDocument Document {
get {
if (this.Parent != null) {
@ -50,30 +59,54 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -50,30 +59,54 @@ namespace ICSharpCode.AvalonEdit.XmlParser
}
/// <summary> Occurs when the value of any local properties changes. Nested changes do not cause the event to occur </summary>
public event EventHandler LocalDataChanged;
public event EventHandler Changed;
protected void OnLocalDataChanged()
protected void OnChanged()
{
LogDom("Local data changed for {0}", this);
if (LocalDataChanged != null) {
LocalDataChanged(this, EventArgs.Empty);
LogDom("Changed {0}", this);
if (Changed != null) {
Changed(this, EventArgs.Empty);
}
RawDocument doc = this.Document;
if (doc != null) {
Document.OnObjectChanged(this);
}
}
public new int EndOffset {
List<XmlParser.SyntaxError> syntaxErrors;
/// <summary>
/// The error that occured in the context of this node (excluding nested nodes)
/// </summary>
public IEnumerable<XmlParser.SyntaxError> SyntaxErrors {
get {
return this.StartOffset + this.Length;
}
set {
this.Length = value - this.StartOffset;
if (syntaxErrors == null) {
return new XmlParser.SyntaxError[] {};
} else {
return syntaxErrors;
}
}
}
internal void AddSyntaxError(XmlParser.SyntaxError error)
{
Assert(error.Object == this);
if (this.syntaxErrors == null) this.syntaxErrors = new List<XmlParser.SyntaxError>();
syntaxErrors.Add(error);
}
public RawObject()
{
this.ReadCallID = new object();
}
protected static void Assert(bool condition)
{
if (!condition) {
throw new Exception("Consistency assertion failed");
}
}
public virtual IEnumerable<RawObject> GetSelfAndAllChildren()
{
return new RawObject[] { this };
@ -88,6 +121,18 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -88,6 +121,18 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// type and sequential position hoping to be luckily right
this.StartOffset = source.StartOffset;
this.EndOffset = source.EndOffset;
// Do not bother comparing - assume changed if non-null
if (this.syntaxErrors != null || source.syntaxErrors != null) {
this.syntaxErrors = new List<XmlParser.SyntaxError>();
foreach(var error in source.SyntaxErrors) {
// 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));
}
// May be called again in derived class - oh, well, nevermind
OnChanged();
}
}
public override string ToString()
@ -180,62 +225,132 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -180,62 +225,132 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// Only these four methods should be used to modify the collection
// See constriants of Parent pointer
/// <summary>
/// To be used exlucively by the parser
/// </summary>
internal void AddChild(RawObject item)
{
this.InsertChildren(this.Children.Count, new RawObject[] {item}.ToList());
AddChildren(new RawObject[] {item}.ToList());
}
/// <summary>
/// To be used exlucively by the parser
/// </summary>
internal void AddChildren(IEnumerable<RawObject> items)
{
this.InsertChildren(this.Children.Count, items.ToList());
// Childs can be only added to newly parsed items
Assert(this.Parent == null);
// Read the list just once
items = items.ToList();
foreach(RawObject item in items) {
// Are we adding some cached item?
// We can *not* modify data of other tree
// It might resurect user deleted nodes, but that is fine
if (item.Parent == null) item.Parent = this;
}
this.Children.InsertItems(this.Children.Count, items.ToList());
}
/// <summary>
/// Insert children, set parent for them and notify the document
/// To be used exclusively by UpdateChildrenFrom.
/// Insert children and keep links consistent.
/// Note: If the nodes are in other part of the document, they will be moved
/// </summary>
void InsertChildren(int index, IList<RawObject> items)
{
if (items.Count == 1) {
LogDom("Inserting {0} at index {1}", items[0], index);
} else {
LogDom("Inserting at index {0}:", index);
foreach(RawObject item in items) LogDom(" {0}", item);
RawDocument document = this.Document;
Assert(document != null);
Assert(!document.IsParsed);
List<RawObject> attachedObjects = new List<RawObject>();
// Remove from the old location and set parent
foreach(RawObject item in items) {
if (item.Parent == null) {
// Dangling object - it was probably just removed from the document during update
LogDom("Inserting dangling {0}", item);
item.Parent = this;
attachedObjects.Add(item);
} else if (item.Document.IsParsed) {
// Adding from parser tree - steal pointer; keep in the parser tree
LogDom("Inserting {0} from parser tree", item);
item.Parent = this;
attachedObjects.Add(item);
} else {
// Adding from user other document location
Assert(item.Document == document); // The parser was reusing object from other document?
LogDom("Inserting {0} from other document location", item);
// Remove from other location
var owingList = ((RawContainer)item.Parent).Children;
owingList.RemoveItems(owingList.IndexOf(item), 1);
// No detach / attach notifications
item.Parent = this;
}
}
foreach(RawObject item in items) item.Parent = this;
// Add it
this.Children.InsertItems(index, items);
RawDocument document = this.Document;
if (document != null) {
foreach(RawObject item in items) {
foreach(RawObject obj in item.GetSelfAndAllChildren()) {
document.OnObjectAttached(obj);
}
// Notify document - do last so that the handler sees up-to-date tree
foreach(RawObject item in attachedObjects) {
foreach(RawObject obj in item.GetSelfAndAllChildren()) {
document.OnObjectAttached(obj);
}
}
}
/// <summary>
/// To be used exclusively by UpdateChildrenFrom.
/// Remove children, set parent to null for them and notify the document
/// </summary>
void RemoveChildrenAt(int index, int count)
{
RawDocument document = this.Document;
Assert(document != null);
Assert(!document.IsParsed);
List<RawObject> removed = new List<RawObject>(count);
for(int i = 0; i < count; i++) {
removed.Add(this.Children[index + i]);
}
// Log the action
if (count == 1) {
LogDom("Removing {0} at index {1}", removed[0], index);
} else {
LogDom("Removing at index {0}:", index);
foreach(RawObject item in removed) LogDom(" {0}", item);
}
foreach(RawObject item in removed) item.Parent = null;
// Null parent pointer
foreach(RawObject item in removed) {
Assert(item.Parent != null);
item.Parent = null;
}
// Remove
this.Children.RemoveItems(index, count);
RawDocument document = this.Document;
if (document != null) {
foreach(RawObject item in removed) {
foreach(RawObject obj in item.GetSelfAndAllChildren()) {
document.OnObjectDettached(obj);
}
// Notify document - do last so that the handler sees up-to-date tree
foreach(RawObject item in removed) {
foreach(RawObject obj in item.GetSelfAndAllChildren()) {
document.OnObjectDettached(obj);
}
}
}
internal void CheckLinksConsistency()
{
foreach(RawObject child in this.Children) {
if (child.Parent == null) throw new Exception("Null parent reference");
if (!(child.Parent == this)) throw new Exception("Inccorect parent reference");
if (child is RawContainer) {
((RawContainer)child).CheckLinksConsistency();
}
}
}
@ -331,8 +446,19 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -331,8 +446,19 @@ namespace ICSharpCode.AvalonEdit.XmlParser
/// </summary>
public class RawDocument: RawContainer
{
/// <summary>
/// Parser tree (as opposed to some user tree).
/// Parser tree can reuse data of other trees as long as it does not modify them
/// (that, it can not set parent pointer if non-null)
/// </summary>
internal bool IsParsed { get; set; }
/// <summary> Occurs when object is added to the document </summary>
public event EventHandler<RawObjectEventArgs> ObjectAttached;
/// <summary> Occurs when object is removed from the document </summary>
public event EventHandler<RawObjectEventArgs> ObjectDettached;
/// <summary> Occurs when local data of object changes </summary>
public event EventHandler<RawObjectEventArgs> ObjectChanged;
internal void OnObjectAttached(RawObject obj)
{
@ -344,6 +470,11 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -344,6 +470,11 @@ namespace ICSharpCode.AvalonEdit.XmlParser
if (ObjectDettached != null) ObjectDettached(this, new RawObjectEventArgs() { Object = obj } );
}
internal void OnObjectChanged(RawObject obj)
{
if (ObjectChanged != null) ObjectChanged(this, new RawObjectEventArgs() { Object = obj } );
}
public override void AcceptVisitor(IXmlVisitor visitor)
{
visitor.VisitDocument(this);
@ -420,7 +551,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -420,7 +551,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
this.OpeningBracket = src.OpeningBracket;
this.Name = src.Name;
this.ClosingBracket = src.ClosingBracket;
OnLocalDataChanged();
OnChanged();
}
}
@ -470,7 +601,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -470,7 +601,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
UpdateXElement(true);
UpdateXElementAttributes(true);
UpdateXElementChildren(true);
this.StartTag.LocalDataChanged += delegate { UpdateXElement(false); };
this.StartTag.Changed += delegate { UpdateXElement(false); };
this.StartTag.Children.CollectionChanged += delegate { UpdateXElementAttributes(false); };
this.Children.CollectionChanged += delegate { UpdateXElementChildren(false); };
}
@ -552,7 +683,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -552,7 +683,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
this.Name = src.Name;
this.EqualsSign = src.EqualsSign;
this.Value = src.Value;
OnLocalDataChanged();
OnChanged();
}
}
@ -566,7 +697,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -566,7 +697,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
xAttr.AddAnnotation(this);
bool deleted = false;
UpdateXAttribute(true, ref deleted);
this.LocalDataChanged += delegate { if (!deleted) UpdateXAttribute(false, ref deleted); };
this.Changed += delegate { if (!deleted) UpdateXAttribute(false, ref deleted); };
}
return xAttr;
}
@ -636,7 +767,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -636,7 +767,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
RawText src = (RawText)source;
if (this.Value != src.Value) {
this.Value = src.Value;
OnLocalDataChanged();
OnChanged();
}
}
@ -646,7 +777,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -646,7 +777,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
XText text = new XText(string.Empty);
text.AddAnnotation(this);
UpdateXText(text, autoUpdate);
if (autoUpdate) this.LocalDataChanged += delegate { UpdateXText(text, autoUpdate); };
if (autoUpdate) this.Changed += delegate { UpdateXText(text, autoUpdate); };
return text;
}

260
src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/XmlParser/XmlParser.cs

@ -88,9 +88,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -88,9 +88,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
/// </remarks>
public class XmlParser
{
// TODO: Error reporting
// TODO: Simple tag matching heuristic
// TODO: Backtracking for unclosed long Text sections
// TODO: Delete some read functions and optimize performance
// TODO: Rewrite ReadText
@ -100,11 +98,13 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -100,11 +98,13 @@ namespace ICSharpCode.AvalonEdit.XmlParser
List<DocumentChangeEventArgs> changesSinceLastParse = new List<DocumentChangeEventArgs>();
// Stored parsed items as long as they are valid
/// <summary> Previously parsed items as long as they are valid </summary>
TextSegmentCollection<RawObject> parsedItems = new TextSegmentCollection<RawObject>();
// Is used to identify what memory range was touched by object
// The default is (StartOffset, EndOffset + 1) which is not stored
/// <summary>
/// Is used to identify what memory range was touched by object
/// The default is (StartOffset, EndOffset + 1) which is not stored
/// </summary>
TextSegmentCollection<TouchedMemoryRange> touchedMemoryRanges = new TextSegmentCollection<TouchedMemoryRange>();
class TouchedMemoryRange: TextSegment
@ -112,6 +112,33 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -112,6 +112,33 @@ namespace ICSharpCode.AvalonEdit.XmlParser
public RawObject TouchedByObject { get; set; }
}
/// <summary>
/// All syntax errors in the user document
/// </summary>
public TextSegmentCollection<SyntaxError> SyntaxErrors { get; private set; }
/// <summary> Information about syntax error that occured during parsing </summary>
public class SyntaxError: TextSegment
{
/// <summary> Object for which the error occured </summary>
public RawObject Object { get; internal set; }
/// <summary> Textual description of the error </summary>
public string Message { get; internal set; }
/// <summary> Any user data </summary>
public object Tag { get; set; }
internal SyntaxError Clone(RawObject newOwner)
{
return new SyntaxError {
Object = newOwner,
Message = Message,
Tag = Tag,
StartOffset = StartOffset,
EndOffset = EndOffset,
};
}
}
/// <summary>
/// Create new parser, but do not parse the text yet.
/// </summary>
@ -120,6 +147,27 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -120,6 +147,27 @@ namespace ICSharpCode.AvalonEdit.XmlParser
this.input = input;
this.userDocument = new RawDocument();
this.userLinqDocument = userDocument.GetXDocument();
this.SyntaxErrors = new TextSegmentCollection<SyntaxError>();
userDocument.ObjectAttached += delegate(object sender, RawObjectEventArgs e) {
foreach(SyntaxError error in e.Object.SyntaxErrors) {
this.SyntaxErrors.Add(error);
}
};
userDocument.ObjectDettached += delegate(object sender, RawObjectEventArgs e) {
foreach(SyntaxError error in e.Object.SyntaxErrors) {
this.SyntaxErrors.Remove(error);
}
};
userDocument.ObjectChanged += delegate(object sender, RawObjectEventArgs e) {
foreach(SyntaxError error in this.SyntaxErrors.ToList()) {
if (error.Object == e.Object) {
this.SyntaxErrors.Remove(error);
}
}
foreach(SyntaxError error in e.Object.SyntaxErrors) {
this.SyntaxErrors.Add(error);
}
};
}
/// <summary>
@ -148,6 +196,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -148,6 +196,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// Update offsets of all items
parsedItems.UpdateOffsets(change);
touchedMemoryRanges.UpdateOffsets(change);
this.SyntaxErrors.UpdateOffsets(change);
// Remove any items affected by the change
Log("Changed offset {0}", change.Offset);
@ -172,11 +221,10 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -172,11 +221,10 @@ namespace ICSharpCode.AvalonEdit.XmlParser
RawDocument parsedDocument = ReadDocument();
// Just in case parse method was called redundantly
if (parsedDocument.ReadCallID != userDocument.ReadCallID) {
PrintStringCacheStats();
RawObject.LogDom("Updating main DOM tree...");
}
PrintStringCacheStats();
RawObject.LogDom("Updating main DOM tree...");
userDocument.UpdateDataFrom(parsedDocument);
userDocument.CheckLinksConsistency();
return userDocument;
}
@ -267,6 +315,23 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -267,6 +315,23 @@ namespace ICSharpCode.AvalonEdit.XmlParser
Log("String cache: Requested {0} ({1} bytes); Saved {2} ({3} bytes); {4}% Saved", stringCacheRequestedCount, stringCacheRequestedSize, stringCacheSavedCount, stringCacheSavedSize, stringCacheRequestedSize == 0 ? 0 : stringCacheSavedSize * 100 / stringCacheRequestedSize);
}
void OnSyntaxError(RawObject obj, string message, params object[] args)
{
OnSyntaxError(obj, currentLocation, currentLocation + 1, message, args);
}
void OnSyntaxError(RawObject obj, int start, int end, string message, params object[] args)
{
if (end <= start) end = start + 1;
Log("Syntax error ({0}-{1}): {2}", start, end, string.Format(message, args));
obj.AddSyntaxError(new SyntaxError() {
Object = obj,
StartOffset = start,
EndOffset = end,
Message = string.Format(message, args),
});
}
string input;
int readingEnd;
// Do not ever set the value from parsing methods
@ -369,10 +434,12 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -369,10 +434,12 @@ namespace ICSharpCode.AvalonEdit.XmlParser
bool TryPeek(string text)
{
if (currentLocation + text.Length > readingEnd) return false;
// Early exit
if (!TryPeek(text[0])) return false;
if (!TryPeek(text[0])) return false; // Early exit
maxTouchedLocation = Math.Max(maxTouchedLocation, currentLocation + (text.Length - 1));
// The following comparison 'touches' the end of file
if (currentLocation + text.Length > readingEnd) return false;
return input.Substring(currentLocation, text.Length) == text;
}
@ -458,6 +525,16 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -458,6 +525,16 @@ namespace ICSharpCode.AvalonEdit.XmlParser
}
}
bool IsValidName(string name)
{
try {
System.Xml.XmlConvert.VerifyName(name);
return true;
} catch (System.Xml.XmlException) {
return false;
}
}
/// <summary>
/// Context: any
/// </summary>
@ -465,6 +542,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -465,6 +542,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
{
RawDocument doc;
if (TryReadFromCacheOrNew(out doc)) return doc;
doc.IsParsed = true;
// TODO: Errors in document structure
doc.StartOffset = currentLocation;
@ -517,21 +595,24 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -517,21 +595,24 @@ namespace ICSharpCode.AvalonEdit.XmlParser
if (element.StartTag.ClosingBracket == ">") {
while(true) {
if (IsEndOfFile()) {
OnSyntaxError(element, "Closing tag for '{0}' expected", element.StartTag.Name);
break;
} else if (TryPeek('<')) {
RawObject content = ReadElementOrTag();
element.AddChild(content);
if (content is RawTag && ((RawTag)content).IsEndTag) break;
RawTag endTag = content as RawTag;
if (endTag != null && endTag.IsEndTag) {
if (endTag.Name != element.StartTag.Name) {
OnSyntaxError(element, endTag.StartOffset + 2, endTag.StartOffset + 2 + endTag.Name.Length, "Name '{0}' expected. End tag must have same name as start tag.", element.StartTag.Name);
}
break;
}
} else {
element.AddChildren(ReadText(RawTextType.CharacterData));
}
}
}
element.EndOffset = currentLocation;
// TODO: Closing tag matches
// TODO: Heuristic on closing
// TODO: ERROR - attribute name may not apper multiple times
OnParsed(element);
return element;
@ -553,15 +634,20 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -553,15 +634,20 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// It identifies the type of tag and parsing behavior for the rest of it
tag.OpeningBracket = ReadOpeningBracket();
if (tag.IsStartTag || tag.IsEndTag) {
if (tag.IsStartTag || tag.IsEndTag || tag.IsProcessingInstruction) {
// Read the name
string name;
if (TryReadName(out name)) tag.Name = name;
// TODO: Error - bad name
// TODO: Error - no name?
// TODO: Error - = or " or ' not expected
if (TryReadName(out name)) {
tag.Name = name;
if (!IsValidName(tag.Name)) {
OnSyntaxError(tag, currentLocation - tag.Name.Length, currentLocation, "The name '{0}' is invalid", tag.Name);
}
} else {
OnSyntaxError(tag, "Element name expected");
}
}
if (tag.IsStartTag || tag.IsEndTag) {
// Read attributes for the tag
while(true) {
// Chech for all forbiden 'name' charcters first - see ReadName
@ -576,34 +662,66 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -576,34 +662,66 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// We have "=\'\"" or name - read attribute
tag.AddChild(ReadAttribulte());
}
} else if (tag.IsDocumentType) {
tag.AddChildren(ReadContentOfDTD());
} else {
int start = currentLocation;
IEnumerable<RawObject> text;
if (tag.IsComment) {
text = ReadText(RawTextType.Comment);
} else if (tag.IsCData) {
text = ReadText(RawTextType.CData);
} else if (tag.IsProcessingInstruction) {
text = ReadText(RawTextType.ProcessingInstruction);
} else if (tag.IsUnknownBang) {
text = ReadText(RawTextType.UnknownBang);
} else {
throw new Exception(string.Format("Unknown opening bracket '{0}'", tag.OpeningBracket));
}
// Enumerate
text = text.ToList();
// Backtrack at complete start
if (IsEndOfFile() || (tag.IsUnknownBang && TryPeek('<'))) {
GoBack(start);
} else {
tag.AddChildren(text);
}
}
// Read closing bracket
string bracket;
if (TryReadClosingBracket(out bracket)) {
tag.ClosingBracket = bracket;
}
// Error check
int brStart = currentLocation - (tag.ClosingBracket ?? string.Empty).Length;
if (tag.Name == null) {
// One error was reported already
} else if (tag.IsStartTag) {
if (tag.ClosingBracket != ">" && tag.ClosingBracket != "/>") OnSyntaxError(tag, brStart, currentLocation, "'>' or '/>' expected");
} else if (tag.IsEndTag) {
if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, currentLocation, "'>' expected");
} else if (tag.IsComment) {
// TODO: Backtrack if file end reached
tag.AddChildren(ReadText(RawTextType.Comment));
if (tag.ClosingBracket != "-->") OnSyntaxError(tag, brStart, currentLocation, "'-->' expected");
} else if (tag.IsCData) {
// TODO: Backtrack if file end reached
tag.AddChildren(ReadText(RawTextType.CData));
if (tag.ClosingBracket != "]]>") OnSyntaxError(tag, brStart, currentLocation, "']]>' expected");
} else if (tag.IsProcessingInstruction) {
string name;
if (TryReadName(out name)) tag.Name = name;
// TODO: Error - bad name
// TODO: Error - no name?
// TODO: Backtrack if file end reached
tag.AddChildren(ReadText(RawTextType.ProcessingInstruction));
if (tag.ClosingBracket != "?>") OnSyntaxError(tag, brStart, currentLocation, "'?>' expected");
} else if (tag.IsUnknownBang) {
// TODO: Backtack if '<' (or end of file)
tag.AddChildren(ReadText(RawTextType.UnknownBang));
if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, currentLocation, "'>' expected");
} else if (tag.IsDocumentType) {
tag.AddChildren(ReadContentOfDTD());
if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, currentLocation, "'>' expected");
} else {
throw new Exception(string.Format("Unknown opening bracket '{0}'", tag.OpeningBracket));
}
// Read closing bracket
string bracket;
if (TryReadClosingBracket(out bracket)) tag.ClosingBracket = bracket;
// TODO: else ERROR - Missing closing bracket
// TODO: check correct closing bracket (special case if end of file)
// Attribute name may not apper multiple times
var duplicates = tag.Children.OfType<RawAttribute>().GroupBy(attr => attr.Name).SelectMany(g => g.Skip(1));
foreach(RawAttribute attr in duplicates) {
OnSyntaxError(tag, attr.StartOffset, attr.EndOffset, "Attribute with name '{0}' already exists", attr.Name);
}
tag.EndOffset = currentLocation;
OnParsed(tag);
@ -633,7 +751,6 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -633,7 +751,6 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// the dtdName includes "<!"
if (TryRead(dtdName.Remove(0, 2))) return dtdName;
}
// TODO: Error - unkown bang tag
return "<!";
}
} else {
@ -716,8 +833,14 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -716,8 +833,14 @@ namespace ICSharpCode.AvalonEdit.XmlParser
// Read name
string name;
if (TryReadName(out name)) attr.Name = name;
// TODO: else ERROR - attribute name expected
if (TryReadName(out name)) {
attr.Name = name;
if (!IsValidName(attr.Name)) {
OnSyntaxError(attr, attr.StartOffset, currentLocation, "The name '{0}' is invalid", attr.Name);
}
} else {
OnSyntaxError(attr, "Attribute name expected");
}
// Read equals sign and surrounding whitespace
int checkpoint = currentLocation;
@ -732,7 +855,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -732,7 +855,7 @@ namespace ICSharpCode.AvalonEdit.XmlParser
attr.EqualsSign = GetText(checkpoint, currentLocation);
} else {
GoBack(checkpoint);
// TODO: ERROR - Equals expected
OnSyntaxError(attr, "'=' expected");
}
// Read attribute value
@ -748,19 +871,19 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -748,19 +871,19 @@ namespace ICSharpCode.AvalonEdit.XmlParser
GoBack(valueStart);
ReadAttributeValue(quoteChar);
if (TryRead(quoteChar)) {
// TODO: ERROR - Attribute value not followed by white space of tag end
OnSyntaxError(attr, "White space or end of tag expected");
} else {
// TODO: ERROR - Quote missing or ws missing after next one
OnSyntaxError(attr, "Quote {0} expected (or add whitespace after the following one)", quoteChar);
}
} else {
// TODO: ERROR - Attribute value not followed by white space of tag end
OnSyntaxError(attr, "White space or end of tag expected");
}
}
} else {
// '<' or end of file
// TODO: ERROR - Attribute value not closed
GoBack(valueStart);
ReadAttributeValue(quoteChar);
OnSyntaxError(attr, "Quote {0} expected", quoteChar);
}
} else {
int valueStart = currentLocation;
@ -768,15 +891,13 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -768,15 +891,13 @@ namespace ICSharpCode.AvalonEdit.XmlParser
TryRead('\"');
TryRead('\'');
if (valueStart == currentLocation) {
// TODO: ERROR - Attribute value expected - quote \" \'
OnSyntaxError(attr, "Attribute value expected");
} else {
// TODO: ERROR - Attribute value must be quoted in \" or \'
OnSyntaxError(attr, valueStart, currentLocation, "Attribute value must be quoted");
}
}
attr.Value = GetText(start, currentLocation);
// TODO: Normalize attribute values
attr.EndOffset = currentLocation;
OnParsed(attr);
@ -841,7 +962,8 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -841,7 +962,8 @@ namespace ICSharpCode.AvalonEdit.XmlParser
/// <summary>
/// Reads text and optionaly separates it into fragments.
/// It can also return empty set for no appropriate text input
/// It can also return empty set for no appropriate text input.
/// Make sure you enumerate it only once
/// </summary>
IEnumerable<RawObject> ReadText(RawTextType type)
{
@ -881,11 +1003,29 @@ namespace ICSharpCode.AvalonEdit.XmlParser @@ -881,11 +1003,29 @@ namespace ICSharpCode.AvalonEdit.XmlParser
if (type == RawTextType.WhiteSpace) {
TryMoveToNonWhiteSpace();
} else if (type == RawTextType.CharacterData) {
// TODO: "]]>" is error
TryMoveTo('<');
while(true) {
TryMoveToAnyOf('<', ']');
if (IsEndOfFile()) break;
if (TryPeek('<')) break;
if (TryPeek(']')) {
if (TryPeek("]]>")) {
OnSyntaxError(text, currentLocation, currentLocation + 3, "']]>' is not allowed in text");
}
TryMoveNext();
continue;
}
}
} else if (type == RawTextType.Comment) {
// TODO: "--" is error
TryMoveTo("-->");
while(true) {
if (TryMoveTo('-')) {
if (TryPeek("-->")) break;
if (TryPeek("--")) {
OnSyntaxError(text, currentLocation, currentLocation + 2, "'--' is not allowed in comment");
}
TryMoveNext();
}
if (IsEndOfFile()) break;
}
} else if (type == RawTextType.CData) {
TryMoveTo("]]>");
} else if (type == RawTextType.ProcessingInstruction) {

Loading…
Cancel
Save