PageRenderTime 78ms CodeModel.GetById 10ms app.highlight 59ms RepoModel.GetById 2ms app.codeStats 0ms

/AvalonEdit/ICSharpCode.AvalonEdit/Xml/TagReader.cs

http://github.com/icsharpcode/ILSpy
C# | 740 lines | 646 code | 41 blank | 53 comment | 187 complexity | 77c8afecec91eee81431195dac97a856 MD5 | raw file
  1// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
  2// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
  3
  4using System;
  5using System.Collections.Generic;
  6using System.Globalization;
  7using System.Linq;
  8using System.Text;
  9
 10namespace ICSharpCode.AvalonEdit.Xml
 11{
 12	class TagReader: TokenReader
 13	{
 14		AXmlParser parser;
 15		TrackedSegmentCollection trackedSegments;
 16		string input;
 17		
 18		public TagReader(AXmlParser parser, string input): base(input)
 19		{
 20			this.parser = parser;
 21			this.trackedSegments = parser.TrackedSegments;
 22			this.input = input;
 23		}
 24		
 25		bool TryReadFromCacheOrNew<T>(out T res) where T: AXmlObject, new()
 26		{
 27			return TryReadFromCacheOrNew(out res, t => true);
 28		}
 29		
 30		bool TryReadFromCacheOrNew<T>(out T res, Predicate<T> condition) where T: AXmlObject, new()
 31		{
 32			T cached = trackedSegments.GetCachedObject<T>(this.CurrentLocation, 0, condition);
 33			if (cached != null) {
 34				Skip(cached.Length);
 35				AXmlParser.Assert(cached.Length > 0, "cached elements must not have zero length");
 36				res = cached;
 37				return true;
 38			} else {
 39				res = new T();
 40				return false;
 41			}
 42		}
 43		
 44		void OnParsed(AXmlObject obj)
 45		{
 46			AXmlParser.Log("Parsed {0}", obj);
 47			trackedSegments.AddParsedObject(obj, this.MaxTouchedLocation > this.CurrentLocation ? (int?)this.MaxTouchedLocation : null);
 48		}
 49		
 50		/// <summary>
 51		/// Read all tags in the document in a flat sequence.
 52		/// It also includes the text between tags and possibly some properly nested Elements from cache.
 53		/// </summary>
 54		public List<AXmlObject> ReadAllTags()
 55		{
 56			List<AXmlObject> stream = new List<AXmlObject>();
 57			
 58			while(true) {
 59				if (IsEndOfFile()) {
 60					break;
 61				} else if (TryPeek('<')) {
 62					AXmlElement elem;
 63					if (TryReadFromCacheOrNew(out elem, e => e.IsProperlyNested)) {
 64						stream.Add(elem);
 65					} else {
 66						stream.Add(ReadTag());
 67					}
 68				} else {
 69					stream.AddRange(ReadText(TextType.CharacterData));
 70				}
 71			}
 72			
 73			return stream;
 74		}
 75		
 76		/// <summary>
 77		/// Context: "&lt;"
 78		/// </summary>
 79		AXmlTag ReadTag()
 80		{
 81			AssertHasMoreData();
 82			
 83			AXmlTag tag;
 84			if (TryReadFromCacheOrNew(out tag)) return tag;
 85			
 86			tag.StartOffset = this.CurrentLocation;
 87			
 88			// Read the opening bracket
 89			// It identifies the type of tag and parsing behavior for the rest of it
 90			tag.OpeningBracket = ReadOpeningBracket();
 91			
 92			if (tag.IsUnknownBang && !TryPeekWhiteSpace())
 93				OnSyntaxError(tag, tag.StartOffset, this.CurrentLocation, "Unknown tag");
 94			
 95			if (tag.IsStartOrEmptyTag || tag.IsEndTag || tag.IsProcessingInstruction) {
 96				// Read the name
 97				string name;
 98				if (TryReadName(out name)) {
 99					if (!IsValidName(name)) {
100						OnSyntaxError(tag, this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name);
101					}
102				} else {
103					OnSyntaxError(tag, "Element name expected");
104				}
105				tag.Name = name;
106			} else {
107				tag.Name = string.Empty;
108			}
109			
110			bool isXmlDeclr = tag.StartOffset == 0 && tag.Name == "xml";
111			
112			if (tag.IsStartOrEmptyTag || tag.IsEndTag || isXmlDeclr) {
113				// Read attributes for the tag
114				while(true) {					
115					// Chech for all forbiden 'name' charcters first - see ReadName
116					if (IsEndOfFile()) break;
117					if (TryPeekWhiteSpace()) {
118						tag.AddChildren(ReadText(TextType.WhiteSpace));
119						continue;  // End of file might be next
120					}
121					if (TryPeek('<')) break;
122					string endBr;
123					int endBrStart = this.CurrentLocation; // Just peek
124					if (TryReadClosingBracket(out endBr)) {  // End tag
125						GoBack(endBrStart);
126						break;
127					}
128					
129					// We have "=\'\"" or name - read attribute
130					AXmlAttribute attr = ReadAttribulte();
131					tag.AddChild(attr);
132					if (tag.IsEndTag)
133						OnSyntaxError(tag, attr.StartOffset, attr.EndOffset, "Attribute not allowed in end tag.");
134				}
135			} else if (tag.IsDocumentType) {
136				tag.AddChildren(ReadContentOfDTD());
137			} else {
138				int start = this.CurrentLocation;
139				IEnumerable<AXmlObject> text;
140				if (tag.IsComment) {
141					text = ReadText(TextType.Comment);
142				} else if (tag.IsCData) {
143					text = ReadText(TextType.CData);
144				} else if (tag.IsProcessingInstruction) {
145					text = ReadText(TextType.ProcessingInstruction);
146				} else if (tag.IsUnknownBang) {
147					text = ReadText(TextType.UnknownBang);
148				} else {
149					throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket));
150				}
151				// Enumerate
152				text = text.ToList();
153				// Backtrack at complete start
154				if (IsEndOfFile() || (tag.IsUnknownBang && TryPeek('<'))) {
155					GoBack(start);
156				} else {
157					tag.AddChildren(text);
158				}
159			}
160			
161			// Read closing bracket
162			string bracket;
163			TryReadClosingBracket(out bracket);
164			tag.ClosingBracket = bracket;
165			
166			// Error check
167			int brStart = this.CurrentLocation - (tag.ClosingBracket ?? string.Empty).Length;
168			int brEnd = this.CurrentLocation;
169			if (tag.Name == null) {
170				// One error was reported already
171			} else if (tag.IsStartOrEmptyTag) {
172				if (tag.ClosingBracket != ">" && tag.ClosingBracket != "/>") OnSyntaxError(tag, brStart, brEnd, "'>' or '/>' expected");
173			} else if (tag.IsEndTag) {
174				if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, brEnd, "'>' expected");
175			} else if (tag.IsComment) {
176				if (tag.ClosingBracket != "-->") OnSyntaxError(tag, brStart, brEnd, "'-->' expected");
177			} else if (tag.IsCData) {
178				if (tag.ClosingBracket != "]]>") OnSyntaxError(tag, brStart, brEnd, "']]>' expected");
179			} else if (tag.IsProcessingInstruction) {
180				if (tag.ClosingBracket != "?>") OnSyntaxError(tag, brStart, brEnd, "'?>' expected");
181			} else if (tag.IsUnknownBang) {
182				if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, brEnd, "'>' expected");
183			} else if (tag.IsDocumentType) {
184				if (tag.ClosingBracket != ">") OnSyntaxError(tag, brStart, brEnd, "'>' expected");
185			} else {
186				throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket));
187			}
188			
189			// Attribute name may not apper multiple times
190			var duplicates = tag.Children.OfType<AXmlAttribute>().GroupBy(attr => attr.Name).SelectMany(g => g.Skip(1));
191			foreach(AXmlAttribute attr in duplicates) {
192				OnSyntaxError(tag, attr.StartOffset, attr.EndOffset, "Attribute with name '{0}' already exists", attr.Name);
193			}
194			
195			tag.EndOffset = this.CurrentLocation;
196			
197			OnParsed(tag);
198			return tag;
199		}
200		
201		/// <summary>
202		/// Reads any of the know opening brackets.  (only full bracket)
203		/// Context: "&lt;"
204		/// </summary>
205		string ReadOpeningBracket()
206		{
207			// We are using a lot of string literals so that the memory instances are shared
208			//int start = this.CurrentLocation;
209			if (TryRead('<')) {
210				if (TryRead('/')) {
211					return "</";
212				} else if (TryRead('?')) {
213					return "<?";
214				} else if (TryRead('!')) {
215					if (TryRead("--")) {
216						return "<!--";
217					} else if (TryRead("[CDATA[")) {
218						return "<![CDATA[";
219					} else {
220						foreach(string dtdName in AXmlTag.DtdNames) {
221							// the dtdName includes "<!"
222							if (TryRead(dtdName.Remove(0, 2))) return dtdName;
223						}
224						return "<!";
225					}
226				} else {
227					return "<";
228				}
229			} else {
230				throw new InternalException("'<' expected");
231			}
232		}
233		
234		/// <summary>
235		/// Reads any of the know closing brackets.  (only full bracket)
236		/// Context: any
237		/// </summary>
238		bool TryReadClosingBracket(out string bracket)
239		{
240			// We are using a lot of string literals so that the memory instances are shared
241			if (TryRead('>')) {
242				bracket = ">";
243			} else 	if (TryRead("/>")) {
244				bracket = "/>";
245			} else 	if (TryRead("?>")) {
246				bracket = "?>";
247			} else if (TryRead("-->")) {
248				bracket = "-->";
249			} else if (TryRead("]]>")) {
250				bracket = "]]>";
251			} else {
252				bracket = string.Empty;
253				return false;
254			}
255			return true;
256		}
257		
258		IEnumerable<AXmlObject> ReadContentOfDTD()
259		{
260			int start = this.CurrentLocation;
261			while(true) {
262				if (IsEndOfFile()) break;            // End of file
263				TryMoveToNonWhiteSpace();            // Skip whitespace
264				if (TryRead('\'')) TryMoveTo('\'');  // Skip single quoted string TODO: Bug
265				if (TryRead('\"')) TryMoveTo('\"');  // Skip single quoted string
266				if (TryRead('[')) {                  // Start of nested infoset
267					// Reading infoset
268					while(true) {
269						if (IsEndOfFile()) break;
270						TryMoveToAnyOf('<', ']');
271						if (TryPeek('<')) {
272							if (start != this.CurrentLocation) {  // Two following tags
273								yield return MakeText(start, this.CurrentLocation);
274							}
275							yield return ReadTag();
276							start = this.CurrentLocation;
277						}
278						if (TryPeek(']')) break;
279					}
280				}
281				TryRead(']');                        // End of nested infoset
282				if (TryPeek('>')) break;             // Proper closing
283				if (TryPeek('<')) break;             // Malformed XML
284				TryMoveNext();                       // Skip anything else
285			}
286			if (start != this.CurrentLocation) {
287				yield return MakeText(start, this.CurrentLocation);
288			}
289		}
290		
291		/// <summary>
292		/// Context: name or "=\'\""
293		/// </summary>
294		AXmlAttribute ReadAttribulte()
295		{
296			AssertHasMoreData();
297			
298			AXmlAttribute attr;
299			if (TryReadFromCacheOrNew(out attr)) return attr;
300			
301			attr.StartOffset = this.CurrentLocation;
302			
303			// Read name
304			string name;
305			if (TryReadName(out name)) {
306				if (!IsValidName(name)) {
307					OnSyntaxError(attr, this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name);
308				}
309			} else {
310				OnSyntaxError(attr, "Attribute name expected");
311			}
312			attr.Name = name;
313			
314			// Read equals sign and surrounding whitespace
315			int checkpoint = this.CurrentLocation;
316			TryMoveToNonWhiteSpace();
317			if (TryRead('=')) {
318				int chk2 = this.CurrentLocation;
319				TryMoveToNonWhiteSpace();
320				if (!TryPeek('"') && !TryPeek('\'')) {
321					// Do not read whitespace if quote does not follow
322					GoBack(chk2);
323				}
324				attr.EqualsSign = GetText(checkpoint, this.CurrentLocation);
325			} else {
326				GoBack(checkpoint);
327				OnSyntaxError(attr, "'=' expected");
328				attr.EqualsSign = string.Empty;
329			}
330			
331			// Read attribute value
332			int start = this.CurrentLocation;
333			char quoteChar = TryPeek('"') ? '"' : '\'';
334			bool startsWithQuote;
335			if (TryRead(quoteChar)) {
336				startsWithQuote = true;
337				int valueStart = this.CurrentLocation;
338				TryMoveToAnyOf(quoteChar, '<');
339				if (TryRead(quoteChar)) {
340					if (!TryPeekAnyOf(' ', '\t', '\n', '\r', '/', '>', '?')) {
341						if (TryPeekPrevious('=', 2) || (TryPeekPrevious('=', 3) && TryPeekPrevious(' ', 2))) {
342							// This actually most likely means that we are in the next attribute value
343							GoBack(valueStart);
344							ReadAttributeValue(quoteChar);
345							if (TryRead(quoteChar)) {
346								OnSyntaxError(attr, "White space or end of tag expected");
347							} else {
348								OnSyntaxError(attr, "Quote {0} expected (or add whitespace after the following one)", quoteChar);
349							}
350						} else {
351							OnSyntaxError(attr, "White space or end of tag expected");
352						}
353					}
354				} else {
355					// '<' or end of file
356					GoBack(valueStart);
357					ReadAttributeValue(quoteChar);
358					OnSyntaxError(attr, "Quote {0} expected", quoteChar);
359				}
360			} else {
361				startsWithQuote = false;
362				int valueStart = this.CurrentLocation;
363				ReadAttributeValue(null);
364				TryRead('\"');
365				TryRead('\'');
366				if (valueStart == this.CurrentLocation) {
367					OnSyntaxError(attr, "Attribute value expected");
368				} else {
369					OnSyntaxError(attr, valueStart, this.CurrentLocation, "Attribute value must be quoted");
370				}
371			}
372			attr.QuotedValue = GetText(start, this.CurrentLocation);
373			attr.Value = Unquote(attr.QuotedValue);
374			attr.Value = Dereference(attr, attr.Value, startsWithQuote ? start + 1 : start);
375			
376			attr.EndOffset = this.CurrentLocation;
377			
378			OnParsed(attr);
379			return attr;
380		}
381		
382		/// <summary>
383		/// Read everything up to quote (excluding), opening/closing tag or attribute signature
384		/// </summary>
385		void ReadAttributeValue(char? quote)
386		{
387			while(true) {
388				if (IsEndOfFile()) return;
389				// What is next?
390				int start = this.CurrentLocation;
391				TryMoveToNonWhiteSpace();  // Read white space (if any)
392				if (quote.HasValue) {
393					if (TryPeek(quote.Value)) return;
394				} else {
395					if (TryPeek('"') || TryPeek('\'')) return;
396				}
397				// Opening/closing tag
398				string endBr;
399				if (TryPeek('<') || TryReadClosingBracket(out endBr)) {
400					GoBack(start);
401					return;
402				}
403				// Try reading attribute signature
404				string name;
405				if (TryReadName(out name)) {
406					int nameEnd = this.CurrentLocation;
407					if (TryMoveToNonWhiteSpace() && TryRead("=") &&
408					    TryMoveToNonWhiteSpace() && TryPeekAnyOf('"', '\''))
409					{
410						// Start of attribute.  Great
411						GoBack(start);
412						return;  // Done
413					} else {
414						// Just some gargabe - make it part of the value
415						GoBack(nameEnd);
416						continue;  // Read more
417					}
418				}
419				TryMoveNext(); // Accept everyting else
420			}
421		}
422		
423		AXmlText MakeText(int start, int end)
424		{
425			AXmlParser.DebugAssert(end > start, "Empty text");
426			
427			AXmlText text = new AXmlText() {
428				StartOffset = start,
429				EndOffset = end,
430				EscapedValue = GetText(start, end),
431				Type = TextType.Other
432			};
433			
434			OnParsed(text);
435			return text;
436		}
437		
438		const int maxEntityLength = 16; // The longest build-in one is 10 ("&#1114111;")
439		const int maxTextFragmentSize = 64;
440		const int lookAheadLength = (3 * maxTextFragmentSize) / 2; // More so that we do not get small "what was inserted" fragments
441		
442		/// <summary>
443		/// Reads text and optionaly separates it into fragments.
444		/// It can also return empty set for no appropriate text input.
445		/// Make sure you enumerate it only once
446		/// </summary>
447		IEnumerable<AXmlObject> ReadText(TextType type)
448		{
449			bool lookahead = false;
450			while(true) {
451				AXmlText text;
452				if (TryReadFromCacheOrNew(out text, t => t.Type == type)) {
453					// Cached text found
454					yield return text;
455					continue; // Read next fragment;  the method can handle "no text left"
456				}
457				text.Type = type;
458				
459				// Limit the reading to just a few characters
460				// (the first character not to be read)
461				int fragmentEnd = Math.Min(this.CurrentLocation + maxTextFragmentSize, this.InputLength);
462				
463				// Look if some futher text has been already processed and align so that
464				// we hit that chache point.  It is expensive so it is off for the first run
465				if (lookahead) {
466					// Note: Must fit entity
467					AXmlObject nextFragment = trackedSegments.GetCachedObject<AXmlText>(this.CurrentLocation + maxEntityLength, lookAheadLength - maxEntityLength, t => t.Type == type);
468					if (nextFragment != null) {
469						fragmentEnd = Math.Min(nextFragment.StartOffset, this.InputLength);
470						AXmlParser.Log("Parsing only text ({0}-{1}) because later text was already processed", this.CurrentLocation, fragmentEnd);
471					}
472				}
473				lookahead = true;
474				
475				text.StartOffset = this.CurrentLocation;
476				int start = this.CurrentLocation;
477				
478				// Whitespace would be skipped anyway by any operation
479				TryMoveToNonWhiteSpace(fragmentEnd);
480				int wsEnd = this.CurrentLocation;
481				
482				// Try move to the terminator given by the context
483				if (type == TextType.WhiteSpace) {
484					TryMoveToNonWhiteSpace(fragmentEnd);
485				} else if (type == TextType.CharacterData) {
486					while(true) {
487						if (!TryMoveToAnyOf(new char[] {'<', ']'}, fragmentEnd)) break; // End of fragment
488						if (TryPeek('<')) break;
489						if (TryPeek(']')) {
490							if (TryPeek("]]>")) {
491								OnSyntaxError(text, this.CurrentLocation, this.CurrentLocation + 3, "']]>' is not allowed in text");
492							}
493							TryMoveNext();
494							continue;
495						}
496						throw new Exception("Infinite loop");
497					}
498				} else 	if (type == TextType.Comment) {
499					// Do not report too many errors
500					bool errorReported = false;
501					while(true) {
502						if (!TryMoveTo('-', fragmentEnd)) break; // End of fragment
503						if (TryPeek("-->")) break;
504						if (TryPeek("--") && !errorReported) {
505							OnSyntaxError(text, this.CurrentLocation, this.CurrentLocation + 2, "'--' is not allowed in comment");
506							errorReported = true;
507						}
508						TryMoveNext();
509					}
510				} else if (type == TextType.CData) {
511					while(true) {
512						// We can not use use TryMoveTo("]]>", fragmentEnd) because it may incorectly accept "]" at the end of fragment
513						if (!TryMoveTo(']', fragmentEnd)) break; // End of fragment
514						if (TryPeek("]]>")) break;
515						TryMoveNext();
516					}
517				} else if (type == TextType.ProcessingInstruction) {
518					while(true) {
519						if (!TryMoveTo('?', fragmentEnd)) break; // End of fragment
520						if (TryPeek("?>")) break;
521						TryMoveNext();
522					}
523				} else if (type == TextType.UnknownBang) {
524					TryMoveToAnyOf(new char[] {'<', '>'}, fragmentEnd);
525				} else {
526					throw new Exception("Uknown type " + type);
527				}
528				
529				text.ContainsOnlyWhitespace = (wsEnd == this.CurrentLocation);
530				
531				// Terminal found or real end was reached;
532				bool finished = this.CurrentLocation < fragmentEnd || IsEndOfFile();
533				
534				if (!finished) {
535					// We have to continue reading more text fragments
536					
537					// If there is entity reference, make sure the next segment starts with it to prevent framentation
538					int entitySearchStart = Math.Max(start + 1 /* data for us */, this.CurrentLocation - maxEntityLength);
539					int entitySearchLength = this.CurrentLocation - entitySearchStart;
540					if (entitySearchLength > 0) {
541						// Note that LastIndexOf works backward
542						int entityIndex = input.LastIndexOf('&', this.CurrentLocation - 1, entitySearchLength);
543						if (entityIndex != -1) {
544							GoBack(entityIndex);
545						}
546					}
547				}
548				
549				text.EscapedValue = GetText(start, this.CurrentLocation);
550				if (type == TextType.CharacterData) {
551					// Normalize end of line first
552					text.Value = Dereference(text, NormalizeEndOfLine(text.EscapedValue), start);
553				} else {
554					text.Value = text.EscapedValue;
555				}
556				text.EndOffset = this.CurrentLocation;
557				
558				if (text.EscapedValue.Length > 0) {
559					OnParsed(text);
560					yield return text;
561				}
562				
563				if (finished) {
564					yield break;
565				}
566			}
567		}
568		
569		#region Helper methods
570		
571		void OnSyntaxError(AXmlObject obj, string message, params object[] args)
572		{
573			OnSyntaxError(obj, this.CurrentLocation, this.CurrentLocation + 1, message, args);
574		}
575		
576		public static void OnSyntaxError(AXmlObject obj, int start, int end, string message, params object[] args)
577		{
578			if (end <= start) end = start + 1;
579			string formattedMessage = string.Format(CultureInfo.InvariantCulture, message, args);
580			AXmlParser.Log("Syntax error ({0}-{1}): {2}", start, end, formattedMessage);
581			obj.AddSyntaxError(new SyntaxError() {
582			                   	Object = obj,
583			                   	StartOffset = start,
584			                   	EndOffset = end,
585			                   	Message = formattedMessage,
586			                   });
587		}
588		
589		static bool IsValidName(string name)
590		{
591			try {
592				System.Xml.XmlConvert.VerifyName(name);
593				return true;
594			} catch (System.Xml.XmlException) {
595				return false;
596			}
597		}
598		
599		/// <summary> Remove quoting from the given string </summary>
600		static string Unquote(string quoted)
601		{
602			if (string.IsNullOrEmpty(quoted)) return string.Empty;
603			char first = quoted[0];
604			if (quoted.Length == 1) return (first == '"' || first == '\'') ? string.Empty : quoted;
605			char last  = quoted[quoted.Length - 1];
606			if (first == '"' || first == '\'') {
607				if (first == last) {
608					// Remove both quotes
609					return quoted.Substring(1, quoted.Length - 2);
610				} else {
611					// Remove first quote
612					return quoted.Remove(0, 1);
613				}
614			} else {
615				if (last == '"' || last == '\'') {
616					// Remove last quote
617					return quoted.Substring(0, quoted.Length - 1);
618				} else {
619					// Keep whole string
620					return quoted;
621				}
622			}
623		}
624		
625		static string NormalizeEndOfLine(string text)
626		{
627			return text.Replace("\r\n", "\n").Replace("\r", "\n");
628		}
629		
630		string Dereference(AXmlObject owner, string text, int textLocation)
631		{
632			StringBuilder sb = null;  // The dereferenced text so far (all up to 'curr')
633			int curr = 0;
634			while(true) {
635				// Reached end of input
636				if (curr == text.Length) {
637					if (sb != null) {
638						return sb.ToString();
639					} else {
640						return text;
641					}
642				}
643				
644				// Try to find reference
645				int start = text.IndexOf('&', curr);
646				
647				// No more references found
648				if (start == -1) {
649					if (sb != null) {
650						sb.Append(text, curr, text.Length - curr); // Add rest
651						return sb.ToString();
652					} else {
653						return text;
654					}
655				}
656				
657				// Append text before the enitiy reference
658				if (sb == null) sb = new StringBuilder(text.Length);
659				sb.Append(text, curr, start - curr);
660				curr = start;
661				
662				// Process the entity
663				int errorLoc = textLocation + sb.Length;
664				          
665				// Find entity name
666				int end = text.IndexOfAny(new char[] {'&', ';'}, start + 1, Math.Min(maxEntityLength, text.Length - (start + 1)));
667				if (end == -1 || text[end] == '&') {
668					// Not found
669					OnSyntaxError(owner, errorLoc, errorLoc + 1, "Entity reference must be terminated with ';'");
670					// Keep '&'
671					sb.Append('&');
672					curr++;
673					continue;  // Restart and next character location
674				}
675				string name = text.Substring(start + 1, end - (start + 1));
676				
677				// Resolve the name
678				string replacement;
679				if (name.Length == 0) {
680					replacement = null;
681					OnSyntaxError(owner, errorLoc + 1, errorLoc + 1, "Entity name expected");
682				} else if (name == "amp") {
683					replacement = "&";
684				} else if (name == "lt") {
685					replacement = "<";
686				} else if (name == "gt") {
687					replacement = ">";
688				} else if (name == "apos") {
689					replacement = "'";
690				} else if (name == "quot") {
691					replacement = "\"";
692				} else if (name.Length > 0 && name[0] == '#') {
693					int num;
694					if (name.Length > 1 && name[1] == 'x') {
695						if (!int.TryParse(name.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture.NumberFormat, out num)) {
696							num = -1;
697							OnSyntaxError(owner, errorLoc + 3, errorLoc + 1 + name.Length, "Hexadecimal code of unicode character expected");
698						}
699					} else {
700						if (!int.TryParse(name.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture.NumberFormat, out num)) {
701							num = -1;
702							OnSyntaxError(owner, errorLoc + 2, errorLoc + 1 + name.Length, "Numeric code of unicode character expected");
703						}
704					}
705					if (num != -1) {
706						try {
707							replacement = char.ConvertFromUtf32(num);
708						} catch (ArgumentOutOfRangeException) {
709							replacement = null;
710							OnSyntaxError(owner, errorLoc + 2, errorLoc + 1 + name.Length, "Invalid unicode character U+{0:X} ({0})", num);
711						}
712					} else {
713						replacement = null;
714					}
715				} else if (!IsValidName(name)) {
716					replacement = null;
717					OnSyntaxError(owner, errorLoc + 1, errorLoc + 1, "Invalid entity name");
718				} else {
719					replacement = null;
720					if (parser.UnknownEntityReferenceIsError) {
721						OnSyntaxError(owner, errorLoc, errorLoc + 1 + name.Length + 1, "Unknown entity reference '{0}'", name);
722					}
723				}
724				
725				// Append the replacement to output
726				if (replacement != null) {
727					sb.Append(replacement);
728				} else {
729					sb.Append('&');
730					sb.Append(name);
731					sb.Append(';');
732				}
733				curr = end + 1;
734				continue;
735			}
736		}
737		
738		#endregion
739	}
740}