/// <summary> /// Gets the property associated with the specified key, if /// present. /// </summary> /// <param name="name">The name of the property to get.</param> /// <param name="result">When this method returns, contains the /// value associated with the specified key, if the key is found; /// otherwise, the default <see cref="MarkupValue"/>. This /// parameter is passed uninitialized.</param> /// <returns><see langword="true"/> if the <see /// cref="MarkupAttributeMarker"/> contains an element with the /// specified key; otherwise, <see langword="false"/>.</returns> public bool TryGetProperty(string name, out MarkupValue result) { foreach (var prop in this.Properties) { if (prop.Name.Equals(name)) { result = prop.Value; return(true); } } result = default; return(false); }
/// <summary> /// Initializes a new instance of the <see cref="MarkupProperty"/> /// struct. /// </summary> /// <param name="name">The name of the property.</param> /// <param name="value">The value of the property.</param> internal MarkupProperty(string name, MarkupValue value) { this.Name = name; this.Value = value; }
/// <summary>Parses a line of text, and produces a /// <see cref="MarkupParseResult"/> containing the processed /// text.</summary> /// <param name="input">The text to parse.</param> /// <returns>The resulting markup information.</returns> internal MarkupParseResult ParseMarkup(string input) { if (string.IsNullOrEmpty(input)) { // We got a null input; return an empty markup parse result return(new MarkupParseResult { Text = string.Empty, Attributes = new List <MarkupAttribute>(), }); } this.input = input.Normalize(); this.stringReader = new StringReader(this.input); var stringBuilder = new StringBuilder(); var markers = new List <MarkupAttributeMarker>(); int nextCharacter; char lastCharacter = char.MinValue; // Read the entirety of the line while ((nextCharacter = this.stringReader.Read()) != -1) { char c = (char)nextCharacter; if (c == '\\') { // This may be the start of an escaped bracket ("\[" or // "\]"). Peek ahead to see if it is. var nextC = (char)this.stringReader.Peek(); if (nextC == '[' || nextC == ']') { // It is! We'll discard this '\', and read the next // character as plain text. c = (char)this.stringReader.Read(); stringBuilder.Append(c); this.sourcePosition += 1; continue; } else { // It wasn't an escaped bracket. Continue on, and // parse the '\' as a normal character. } } if (c == '[') { // How long is our current string, in text elements // (i.e. visible glyphs)? this.position = new System.Globalization.StringInfo(stringBuilder.ToString()).LengthInTextElements; // The start of a marker! MarkupAttributeMarker marker = this.ParseAttributeMarker(); markers.Add(marker); var hadPrecedingWhitespaceOrLineStart = this.position == 0 || char.IsWhiteSpace(lastCharacter); bool wasReplacementMarker = false; // Is this a replacement marker? if (marker.Name != null && this.markerProcessors.ContainsKey(marker.Name)) { wasReplacementMarker = true; // Process it and get the replacement text! var replacementText = this.ProcessReplacementMarker(marker); // Insert it into our final string and update our // position accordingly stringBuilder.Append(replacementText); } bool trimWhitespaceIfAble = false; if (hadPrecedingWhitespaceOrLineStart) { // By default, self-closing markers will trim a // single trailing whitespace after it if there was // preceding whitespace. This doesn't happen if the // marker was a replacement marker, or it has a // property "trimwhitespace" (which must be // boolean) set to false. All markers can opt-in to // trailing whitespace trimming by having a // 'trimwhitespace' property set to true. if (marker.Type == TagType.SelfClosing) { trimWhitespaceIfAble = !wasReplacementMarker; } if (marker.TryGetProperty(TrimWhitespaceProperty, out var prop)) { if (prop.Type != MarkupValueType.Bool) { throw new MarkupParseException($"Error parsing line {this.input}: attribute {marker.Name} at position {this.position} has a {prop.Type.ToString().ToLower()} property \"{TrimWhitespaceProperty}\" - this property is required to be a boolean value."); } trimWhitespaceIfAble = prop.BoolValue; } } if (trimWhitespaceIfAble) { // If there's trailing whitespace, and we want to // remove it, do so if (this.PeekWhitespace()) { // Consume the single trailing whitespace // character (and don't update position) this.stringReader.Read(); this.sourcePosition += 1; } } } else { // plain text! add it to the resulting string and // advance the parser's plain-text position stringBuilder.Append(c); this.sourcePosition += 1; } lastCharacter = c; } var attributes = this.BuildAttributesFromMarkers(markers); var characterAttributeIsPresent = false; foreach (var attribute in attributes) { if (attribute.Name == CharacterAttribute) { characterAttributeIsPresent = true; } } if (characterAttributeIsPresent == false) { // Attempt to generate a character attribute from the start // of the string to the first colon var match = EndOfCharacterMarker.Match(this.input); if (match.Success) { var endRange = match.Index + match.Length; var characterName = this.input.Substring(0, match.Index); MarkupValue nameValue = new MarkupValue { Type = MarkupValueType.String, StringValue = characterName, }; MarkupProperty nameProperty = new MarkupProperty(CharacterAttributeNameProperty, nameValue); var characterAttribute = new MarkupAttribute(0, 0, endRange, CharacterAttribute, new[] { nameProperty }); attributes.Add(characterAttribute); } } return(new MarkupParseResult { Text = stringBuilder.ToString(), Attributes = attributes, }); }