// Copyright (c) 2009-2013 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using ICSharpCode.NRefactory.Editor; namespace ICSharpCode.NRefactory.Xml { class TagReader : TokenReader { readonly AXmlParser tagSoupParser; readonly Stack elementNameStack; public TagReader(AXmlParser tagSoupParser, ITextSource input, bool collapseProperlyNestedElements) : base(input) { this.tagSoupParser = tagSoupParser; if (collapseProperlyNestedElements) elementNameStack = new Stack(); } public List ReadAllObjects(CancellationToken cancellationToken) { while (HasMoreData()) { cancellationToken.ThrowIfCancellationRequested(); ReadObject(); } return objects; } public List ReadAllObjectsIncremental(InternalObject[] oldObjects, List reuseMap, CancellationToken cancellationToken) { ObjectIterator oldObjectIterator = new ObjectIterator(oldObjects); int reuseMapIndex = 0; while (reuseMapIndex < reuseMap.Count) { var reuseEntry = reuseMap[reuseMapIndex]; while (this.CurrentLocation < reuseEntry.NewOffset) { cancellationToken.ThrowIfCancellationRequested(); ReadObject(); } if (this.CurrentLocation >= reuseEntry.NewOffset + reuseEntry.Length) { reuseMapIndex++; continue; } Debug.Assert(reuseEntry.NewOffset <= this.CurrentLocation && this.CurrentLocation < reuseEntry.NewOffset + reuseEntry.Length); // reuse the nodes within this reuseEntry starting at oldOffset: int oldOffset = this.CurrentLocation - reuseEntry.NewOffset + reuseEntry.OldOffset; // seek to oldOffset in the oldObjects array: oldObjectIterator.SkipTo(oldOffset); if (oldObjectIterator.CurrentPosition == oldOffset) { // reuse old objects within this reuse entry: int reuseEnd = reuseEntry.OldOffset + reuseEntry.Length; while (oldObjectIterator.CurrentObject != null && oldObjectIterator.CurrentPosition + oldObjectIterator.CurrentObject.LengthTouched < reuseEnd) { StoreObject(oldObjectIterator.CurrentObject); Skip(oldObjectIterator.CurrentObject.Length); oldObjectIterator.MoveNext(); } reuseMapIndex++; // go to next re-use map } else { // We are in a region where old objects are available, but aren't aligned correctly. // Don't skip this reuse entry, and read a single object so that we can re-align ReadObject(); } } while (HasMoreData()) { cancellationToken.ThrowIfCancellationRequested(); ReadObject(); } return objects; } void StoreObject(InternalObject obj) { objects.Add(obj); // Now combine properly-nested elements: if (elementNameStack == null) return; // parsing tag soup InternalTag tag = obj as InternalTag; if (tag == null) return; if (tag.IsEmptyTag) { // the tag is its own element objects[objects.Count - 1] = new InternalElement(tag) { Length = tag.Length, LengthTouched = tag.LengthTouched, IsPropertyNested = true, StartRelativeToParent = tag.StartRelativeToParent, NestedObjects = new [] { tag.SetStartRelativeToParent(0) } }; } else if (tag.IsStartTag) { elementNameStack.Push(tag.Name); } else if (tag.IsEndTag && elementNameStack.Count > 0) { // Now look for the start element: int startIndex = objects.Count - 2; bool ok = false; string expectedName = elementNameStack.Pop(); if (tag.Name == expectedName) { while (startIndex > 0) { var startTag = objects[startIndex] as InternalTag; if (startTag != null) { if (startTag.IsStartTag) { ok = (startTag.Name == expectedName); break; } else if (startTag.IsEndTag) { break; } } startIndex--; } } if (ok) { // We found a correct nesting, let's create an element: InternalObject[] nestedObjects = new InternalObject[objects.Count - startIndex]; int oldStartRelativeToParent = objects[startIndex].StartRelativeToParent; int pos = 0; int maxLengthTouched = 0; for (int i = 0; i < nestedObjects.Length; i++) { nestedObjects[i] = objects[startIndex + i].SetStartRelativeToParent(pos); maxLengthTouched = Math.Max(maxLengthTouched, pos + nestedObjects[i].LengthTouched); pos += nestedObjects[i].Length; } objects.RemoveRange(startIndex, nestedObjects.Length); objects.Add( new InternalElement((InternalTag)nestedObjects[0]) { HasEndTag = true, IsPropertyNested = true, Length = pos, LengthTouched = maxLengthTouched, StartRelativeToParent = oldStartRelativeToParent, NestedObjects = nestedObjects }); } else { // Mismatched name - the nesting isn't properly; // clear the whole stack so that none of the currently open elements are closed as properly-nested. elementNameStack.Clear(); } } } /// /// Reads one or more objects. /// void ReadObject() { if (TryPeek('<')) { ReadTag(); } else { ReadText(TextType.CharacterData); } } #region BeginInternalObject / EndInternalObject List objects = new List(); int internalObjectStartPosition; int CurrentRelativeLocation { get { return CurrentLocation - internalObjectStartPosition; } } struct InternalObjectFrame { public readonly InternalObject InternalObject; public readonly int ParentStartPosition; public InternalObjectFrame(InternalObject internalObject, int parentStartPosition) { this.InternalObject = internalObject; this.ParentStartPosition = parentStartPosition; } } InternalObjectFrame BeginInternalObject(InternalObject internalObject) { return BeginInternalObject(internalObject, this.CurrentLocation); } InternalObjectFrame BeginInternalObject(InternalObject internalObject, int beginLocation) { internalObject.StartRelativeToParent = beginLocation - internalObjectStartPosition; var frame = new InternalObjectFrame(internalObject, internalObjectStartPosition); internalObjectStartPosition = CurrentLocation; return frame; } void EndInternalObject(InternalObjectFrame frame, bool storeNewObject = true) { frame.InternalObject.Length = this.CurrentLocation - internalObjectStartPosition; frame.InternalObject.LengthTouched = this.MaxTouchedLocation - internalObjectStartPosition; frame.InternalObject.SyntaxErrors = GetSyntaxErrors(); if (storeNewObject) StoreObject(frame.InternalObject); internalObjectStartPosition = frame.ParentStartPosition; } #endregion #region Read Tag /// /// Context: "<" /// void ReadTag() { AssertHasMoreData(); int tagStart = this.CurrentLocation; InternalTag tag = new InternalTag(); var frame = BeginInternalObject(tag); // Read the opening bracket // It identifies the type of tag and parsing behavior for the rest of it tag.OpeningBracket = ReadOpeningBracket(); if (tag.IsUnknownBang && !TryPeekWhiteSpace()) OnSyntaxError(tagStart, this.CurrentLocation, "Unknown tag"); if (tag.IsStartOrEmptyTag || tag.IsEndTag || tag.IsProcessingInstruction) { // Read the name TryMoveToNonWhiteSpace(); tag.RelativeNameStart = this.CurrentRelativeLocation; string name; if (TryReadName(out name)) { if (!IsValidName(name)) { OnSyntaxError(this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name); } } else { OnSyntaxError("Element name expected"); } tag.Name = name; } else { tag.Name = string.Empty; } bool isXmlDeclr = tag.Name == "xml" && tag.IsProcessingInstruction; int oldObjectCount = objects.Count; if (tag.IsStartOrEmptyTag || tag.IsEndTag || isXmlDeclr) { // Read attributes for the tag while (HasMoreData()) { // Chech for all forbiden 'name' characters first - see ReadName TryMoveToNonWhiteSpace(); if (TryPeek('<')) break; string endBr; int endBrStart = this.CurrentLocation; // Just peek if (TryReadClosingBracket(out endBr)) { // End tag GoBack(endBrStart); break; } // We have "=\'\"" or name - read attribute int attrStartOffset = this.CurrentLocation; ReadAttribute(); if (tag.IsEndTag) OnSyntaxError(attrStartOffset, this.CurrentLocation, "Attribute not allowed in end tag."); } } else if (tag.IsDocumentType) { ReadContentOfDTD(); } else { int start = this.CurrentLocation; if (tag.IsComment) { ReadText(TextType.Comment); } else if (tag.IsCData) { ReadText(TextType.CData); } else if (tag.IsProcessingInstruction) { ReadText(TextType.ProcessingInstruction); } else if (tag.IsUnknownBang) { ReadText(TextType.UnknownBang); } else { throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket)); } // Backtrack at complete start if (IsEndOfFile() || (tag.IsUnknownBang && TryPeek('<'))) { GoBack(start); objects.RemoveRange(oldObjectCount, objects.Count - oldObjectCount); } } // Read closing bracket string bracket; TryReadClosingBracket(out bracket); tag.ClosingBracket = bracket; // Error check int brStart = this.CurrentLocation - (tag.ClosingBracket ?? string.Empty).Length; int brEnd = this.CurrentLocation; if (tag.Name == null) { // One error was reported already } else if (tag.IsStartOrEmptyTag) { if (tag.ClosingBracket != ">" && tag.ClosingBracket != "/>") OnSyntaxError(brStart, brEnd, "'>' or '/>' expected"); } else if (tag.IsEndTag) { if (tag.ClosingBracket != ">") OnSyntaxError(brStart, brEnd, "'>' expected"); } else if (tag.IsComment) { if (tag.ClosingBracket != "-->") OnSyntaxError(brStart, brEnd, "'-->' expected"); } else if (tag.IsCData) { if (tag.ClosingBracket != "]]>") OnSyntaxError(brStart, brEnd, "']]>' expected"); } else if (tag.IsProcessingInstruction) { if (tag.ClosingBracket != "?>") OnSyntaxError(brStart, brEnd, "'?>' expected"); } else if (tag.IsUnknownBang) { if (tag.ClosingBracket != ">") OnSyntaxError(brStart, brEnd, "'>' expected"); } else if (tag.IsDocumentType) { if (tag.ClosingBracket != ">") OnSyntaxError(brStart, brEnd, "'>' expected"); } else { throw new InternalException(string.Format(CultureInfo.InvariantCulture, "Unknown opening bracket '{0}'", tag.OpeningBracket)); } // Attribute name may not apper multiple times if (objects.Count > oldObjectCount) { // Move nested objects into tag.NestedObjects: tag.NestedObjects = new InternalObject[objects.Count - oldObjectCount]; objects.CopyTo(oldObjectCount, tag.NestedObjects, 0, tag.NestedObjects.Length); objects.RemoveRange(oldObjectCount, objects.Count - oldObjectCount); // Look for duplicate attributes: HashSet attributeNames = new HashSet(); foreach (var obj in tag.NestedObjects) { InternalAttribute attr = obj as InternalAttribute; if (attr != null && !attributeNames.Add(attr.Name)) { int attrStart = tagStart + attr.StartRelativeToParent; OnSyntaxError(attrStart, attrStart + attr.Name.Length, "Attribute with name '{0}' already exists", attr.Name); } } } EndInternalObject(frame); } #endregion #region Read DTD void ReadContentOfDTD() { int start = this.CurrentLocation; while (HasMoreData()) { 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 (HasMoreData()) { TryMoveToAnyOf('<', ']'); if (TryPeek('<')) { if (start != this.CurrentLocation) { // Two following tags MakeText(start, this.CurrentLocation); } 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) { MakeText(start, this.CurrentLocation); } } void MakeText(int start, int end) { Log.DebugAssert(end > start, "Empty text"); Log.DebugAssert(end == this.CurrentLocation, "end == current location"); InternalText text = new InternalText(); var frame = BeginInternalObject(text, start); text.Type = TextType.Other; text.Value = GetText(start, end); EndInternalObject(frame); } #endregion #region Read Brackets /// /// 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; } #endregion #region Attributes /// /// Context: name or "=\'\"" /// void ReadAttribute() { AssertHasMoreData(); InternalAttribute attr = new InternalAttribute(); var frame = BeginInternalObject(attr); // Read name string name; if (TryReadName(out name)) { if (!IsValidName(name)) { OnSyntaxError(this.CurrentLocation - name.Length, this.CurrentLocation, "The name '{0}' is invalid", name); } } else { OnSyntaxError("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.EqualsSignLength = this.CurrentLocation - checkpoint; } else { GoBack(checkpoint); OnSyntaxError("'=' expected"); attr.EqualsSignLength = 0; } // 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("White space or end of tag expected"); } else { OnSyntaxError("Quote {0} expected (or add whitespace after the following one)", quoteChar); } } else { OnSyntaxError("White space or end of tag expected"); } } } else { // '<' or end of file GoBack(valueStart); ReadAttributeValue(quoteChar); OnSyntaxError("Quote {0} expected", quoteChar); } } else { startsWithQuote = false; int valueStart = this.CurrentLocation; ReadAttributeValue(null); TryRead('\"'); TryRead('\''); if (valueStart == this.CurrentLocation) { OnSyntaxError("Attribute value expected"); } else { OnSyntaxError(valueStart, this.CurrentLocation, "Attribute value must be quoted"); } } string val = GetText(start, this.CurrentLocation); val = Unquote(val); attr.Value = Dereference(val, startsWithQuote ? start + 1 : start); EndInternalObject(frame); } /// /// Read everything up to quote (excluding), opening/closing tag or attribute signature /// void ReadAttributeValue(char? quote) { while (HasMoreData()) { // 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 if (TryReadName()) { 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 } } /// 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; } } } #endregion #region Text /// /// Reads text. /// void ReadText(TextType type) { var text = new InternalText(); var frame = BeginInternalObject(text); text.Type = type; int start = this.CurrentLocation; int fragmentEnd = inputLength; // 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(this.CurrentLocation, this.CurrentLocation + 3, "']]>' is not allowed in text"); } TryMoveNext(); continue; } throw new InternalException("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(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 InternalException("Unknown type " + type); } text.ContainsOnlyWhitespace = (wsEnd == this.CurrentLocation); string escapedValue = GetText(start, this.CurrentLocation); if (type == TextType.CharacterData) { text.Value = Dereference(escapedValue, start); } else { text.Value = escapedValue; } text.Value = GetCachedString(text.Value); EndInternalObject(frame, storeNewObject: this.CurrentLocation > start); } #endregion #region Dereference const int maxEntityLength = 16; // The longest built-in one is 10 ("􏿿") string Dereference(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(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(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(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(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(errorLoc + 2, errorLoc + 1 + name.Length, "Invalid unicode character U+{0:X} ({0})", num); } } else { replacement = null; } } else if (!IsValidName(name)) { replacement = null; OnSyntaxError(errorLoc + 1, errorLoc + 1, "Invalid entity name"); } else { replacement = null; if (tagSoupParser.UnknownEntityReferenceIsError) { OnSyntaxError(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 #region Syntax Errors List syntaxErrors = new List(); InternalSyntaxError[] GetSyntaxErrors() { if (syntaxErrors.Count > 0) { var arr = syntaxErrors.ToArray(); syntaxErrors.Clear(); return arr; } else { return null; } } void OnSyntaxError(string message, params object[] args) { OnSyntaxError(this.CurrentLocation, this.CurrentLocation + 1, message, args); } void OnSyntaxError(int start, int end, string message, params object[] args) { if (end <= start) end = start + 1; string formattedMessage = string.Format(CultureInfo.InvariantCulture, message, args); Log.WriteLine("Syntax error ({0}-{1}): {2}", start, end, formattedMessage); syntaxErrors.Add(new InternalSyntaxError(start - internalObjectStartPosition, end - internalObjectStartPosition, formattedMessage)); } #endregion #region Helper functions internal static bool IsValidName(string name) { try { System.Xml.XmlConvert.VerifyName(name); return true; } catch (System.Xml.XmlException) { return false; } } #endregion } }