Exemplo n.º 1
0
        /// <summary>
        /// Parses a marker and generates replacement text to insert into
        /// the plain text.
        /// </summary>
        /// <param name="marker">The marker to parse.</param>
        /// <returns>The replacement text to insert.</returns>
        private string ProcessReplacementMarker(MarkupAttributeMarker marker)
        {
            // If it's not an open or self-closing marker, we have no text
            // to insert, so return the empty string
            if (marker.Type != TagType.Open && marker.Type != TagType.SelfClosing)
            {
                return(string.Empty);
            }

            // this is an attribute that we want to replace with text!

            // if this is an opening marker, we read up to the closing
            // marker, the close-all marker, or the end of the string; this
            // becomes the value of a property called "contents", and then
            // we perform the replacement
            if (marker.Type == TagType.Open)
            {
                // Read everything up to the closing tag
                string markerContents = this.ParseRawTextUpToAttributeClose(marker.Name);

                // Add this as a property
                marker.Properties.Add(
                    new MarkupProperty(
                        ReplacementMarkerContents,
                        new MarkupValue
                {
                    StringValue = markerContents,
                    Type        = MarkupValueType.String,
                }));
            }

            // Fetch the text that should be inserted into the string at
            // this point
            var replacementText = this.markerProcessors[marker.Name].ReplacementTextForMarker(marker);

            return(replacementText);
        }
Exemplo n.º 2
0
 /// <summary>
 /// Initializes a new instance of the <see cref="MarkupAttribute"/>
 /// struct, using information taken from an opening <see
 /// cref="MarkupAttributeMarker"/>.
 /// </summary>
 /// <param name="openingMarker">The marker that represents the
 /// start of this attribute.</param>
 /// <param name="length">The number of text elements in the plain
 /// text that this attribute covers.</param>
 internal MarkupAttribute(MarkupAttributeMarker openingMarker, int length)
     : this(openingMarker.Position, openingMarker.SourcePosition, length, openingMarker.Name, openingMarker.Properties)
 {
 }
Exemplo n.º 3
0
        /// <summary>
        /// Creates a list of <see cref="MarkupAttribute"/>s from loose
        /// <see cref="MarkupAttributeMarker"/>s.
        /// </summary>
        /// <param name="markers">The collection of markers.</param>
        /// <returns>The list of attributes.</returns>
        /// <throws cref="MarkupParseException">Thrown when a close marker
        /// is encountered, but no corresponding open marker for it
        /// exists.</throws>
        private List <MarkupAttribute> BuildAttributesFromMarkers(List <MarkupAttributeMarker> markers)
        {
            // Using a linked list here because we want to append to the
            // front and be able to walk through it easily
            var unclosedMarkerList = new LinkedList <MarkupAttributeMarker>();

            var attributes = new List <MarkupAttribute>(markers.Count);

            foreach (var marker in markers)
            {
                switch (marker.Type)
                {
                case TagType.Open:
                    // A new marker! Add it to the unclosed list at the
                    // start (because there's a high chance that it
                    // will be closed soon).
                    unclosedMarkerList.AddFirst(marker);
                    break;

                case TagType.Close:
                {
                    // A close marker! Walk back through the
                    // unclosed stack to find the most recent
                    // marker of the same type to find its pair.
                    MarkupAttributeMarker matchedOpenMarker = default;
                    foreach (var openMarker in unclosedMarkerList)
                    {
                        if (openMarker.Name == marker.Name)
                        {
                            // Found a corresponding open!
                            matchedOpenMarker = openMarker;
                            break;
                        }
                    }

                    if (matchedOpenMarker.Name == null)
                    {
                        throw new MarkupParseException($"Unexpected close marker {marker.Name} at position {marker.Position} in line {this.input}");
                    }

                    // This attribute is now closed, so we can
                    // remove the marker from the unmatched list
                    unclosedMarkerList.Remove(matchedOpenMarker);

                    // We can now construct the attribute!
                    var length    = marker.Position - matchedOpenMarker.Position;
                    var attribute = new MarkupAttribute(matchedOpenMarker, length);

                    attributes.Add(attribute);
                }

                break;

                case TagType.SelfClosing:
                {
                    // Self-closing markers create a zero-length
                    // attribute where they appear
                    var attribute = new MarkupAttribute(marker, 0);
                    attributes.Add(attribute);
                }

                break;

                case TagType.CloseAll:
                {
                    // Close all currently open markers

                    // For each marker that we currently have open,
                    // this marker has closed it, so create an
                    // attribute for it
                    foreach (var openMarker in unclosedMarkerList)
                    {
                        var length    = marker.Position - openMarker.Position;
                        var attribute = new MarkupAttribute(openMarker, length);

                        attributes.Add(attribute);
                    }

                    // We've now closed all markers, so we can
                    // clear the unclosed list now
                    unclosedMarkerList.Clear();
                }

                break;
                }
            }

            attributes.Sort(AttributePositionComparison);

            return(attributes);
        }
Exemplo n.º 4
0
        /// <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,
            });
        }