private void AppendRawNode(StringBuilder builder, string source, Shortcode node)
 {
     if (node.OpenBraces == node.CloseBraces)
     {
         builder.Append(source, node.SourceIndex, node.SourceLength + node.CloseBraces);
     }
     else
     {
         builder.Append(source, node.SourceIndex, node.SourceLength + 1);
     }
 }
示例#2
0
        public bool ReadShortcode(out Shortcode shortcode)
        {
            shortcode = null;
            var style = ShortcodeStyle.Open;

            if (_cursor.Char != '[')
            {
                return(false);
            }

            CreateCursor();

            _cursor.Advance();

            // Is it a closing tag?
            if (_cursor.Char == '/')
            {
                style = ShortcodeStyle.Close;

                _cursor.Advance();
            }

            // Reach Eof before end of shortcode
            if (_cursor.Eof)
            {
                DiscardCursor();

                return(false);
            }

            SkipWhiteSpace();

            if (!ReadIdentifier())
            {
                DiscardCursor();

                return(false);
            }

            Token identifier = _token;

            SkipWhiteSpace();

            Dictionary <string, string> arguments = null;

            int argumentIndex = 0;

            // Arguments?
            while (ReadIdentifier() || ReadString())
            {
                // Is it a positioned argument?
                if (_token.Type == "string")
                {
                    arguments ??= new Dictionary <string, string>();

                    arguments[argumentIndex.ToString()] = DecodeString(_token.ToString());

                    argumentIndex += 1;

                    SkipWhiteSpace();
                }
                else
                {
                    var argument = _token;

                    SkipWhiteSpace();

                    if (!ReadEqualSign())
                    {
                        DiscardCursor();

                        return(false);
                    }

                    SkipWhiteSpace();

                    if (ReadString())
                    {
                        arguments ??= new Dictionary <string, string>();

                        arguments[argument.ToString()] = DecodeString(_token.ToString());
                    }
                    else
                    {
                        DiscardCursor();

                        return(false);
                    }

                    SkipWhiteSpace();
                }
            }

            // Is it a self-closing tag?
            if (_cursor.Char == '/' && _cursor.PeekNext() == ']')
            {
                style = ShortcodeStyle.SelfClosing;

                _cursor.Advance();
            }

            // Expect closing bracket
            if (_cursor.Char != ']')
            {
                DiscardCursor();

                return(false);
            }

            // Ignore shortcode if the next char is also ']', making it a comment
            if (_cursor.PeekNext() == ']')
            {
                DiscardCursor();

                return(false);
            }

            shortcode           = new Shortcode(identifier.ToString(), style);
            shortcode.Arguments = new Arguments(arguments);

            PromoteCursor();

            return(true);
        }
示例#3
0
        private Shortcode ParseShortcode()
        {
            // Number of opening braces
            var openBraces = 0;

            // Number of closing braces
            var closeBraces = 0;

            Shortcode shortcode;

            var style = ShortcodeStyle.Open;

            // Start position of the shortcode
            var start = _scanner.Cursor.Position;

            if (!_scanner.ReadChar('['))
            {
                return(null);
            }

            openBraces += 1;

            // Read all '[' so we can detect escaped tags
            while (_scanner.ReadChar('['))
            {
                openBraces += 1;
            }

            // Is it a closing tag?
            if (_scanner.ReadChar('/'))
            {
                style = ShortcodeStyle.Close;
            }

            // Reach Eof before end of shortcode
            if (_scanner.Cursor.Eof)
            {
                _scanner.Cursor.ResetPosition(start);

                return(null);
            }

            _scanner.SkipWhiteSpace();

            if (!_scanner.ReadIdentifier(_result))
            {
                _scanner.Cursor.ResetPosition(start);

                return(null);
            }

            var identifier = _result.Text;

            _scanner.SkipWhiteSpace();

            Dictionary <string, string> arguments = null;

            int argumentIndex = 0;

            // Arguments?
            while (!_scanner.Cursor.Eof)
            {
                // Record location in case it doesn't have a value
                var argumentStart = _scanner.Cursor.Position;

                if (_scanner.ReadQuotedString(_result))
                {
                    arguments ??= CreateArgumentsDictionary();

                    arguments[argumentIndex.ToString()] = Character.DecodeString(_result.Span.Slice(1, _result.Length - 2)).ToString();

                    argumentIndex += 1;
                }
                else if (_scanner.ReadIdentifier(_result))
                {
                    _scanner.SkipWhiteSpace();

                    var argumentName = _result.Text;

                    // It might just be a value
                    if (_scanner.ReadChar('='))
                    {
                        _scanner.SkipWhiteSpace();

                        if (_scanner.ReadQuotedString(_result))
                        {
                            arguments ??= CreateArgumentsDictionary();

                            arguments[argumentName] = Character.DecodeString(_result.Span.Slice(1, _result.Length - 2)).ToString();
                        }
                        else if (_scanner.ReadValue(_result))
                        {
                            arguments ??= CreateArgumentsDictionary();

                            arguments[argumentName] = _result.Text.ToString();
                        }
                        else
                        {
                            _scanner.Cursor.ResetPosition(start);

                            return(null);
                        }
                    }
                    else
                    {
                        // Positional argument that looks like an identifier

                        _scanner.Cursor.ResetPosition(argumentStart);

                        if (_scanner.ReadValue(_result))
                        {
                            arguments ??= CreateArgumentsDictionary();

                            arguments[argumentIndex.ToString()] = _result.Text;

                            argumentIndex += 1;
                        }
                        else
                        {
                            _scanner.Cursor.ResetPosition(start);

                            break;
                        }
                    }
                }
                else if (_scanner.ReadValue(_result))
                {
                    arguments ??= CreateArgumentsDictionary();

                    arguments[argumentIndex.ToString()] = _result.Text;

                    argumentIndex += 1;
                }
                else if (_scanner.Cursor.Match("/]"))
                {
                    style = ShortcodeStyle.SelfClosing;
                    _scanner.Cursor.Advance();
                    break;
                }
                else if (_scanner.Cursor.Match(']'))
                {
                    break;
                }
                else
                {
                    _scanner.Cursor.ResetPosition(start);
                    return(null);
                }

                _scanner.SkipWhiteSpace();
            }

            // If we exited the loop due to EOF, exit
            if (_scanner.Cursor.Eof || !_scanner.ReadChar(']'))
            {
                _scanner.Cursor.ResetPosition(start);
                return(null);
            }

            closeBraces += 1;

            // Read all ']' so we can detect escaped tags
            while (_scanner.ReadChar(']'))
            {
                closeBraces += 1;
            }

            shortcode = new Shortcode(
                identifier,
                style,
                openBraces,
                closeBraces,
                start.Offset,
                _scanner.Cursor.Position - start - 1,
                new Arguments(arguments)
                );

            return(shortcode);
示例#4
0
        public bool ReadShortcode(out Shortcode shortcode)
        {
            // Number of opening braces
            var openBraces = 0;

            // Number of closing braces
            var closeBraces = 0;

            shortcode = null;
            var style = ShortcodeStyle.Open;

            if (_cursor.Char != '[')
            {
                return(false);
            }

            CreateCursor();

            // Start position of the shortcode
            var index = _cursor.Offset;

            // Read all '[' so we can detect escaped tags
            do
            {
                openBraces += 1;
                _cursor.Advance();
            } while (_cursor.Char == '[');

            // Is it a closing tag?
            if (_cursor.Char == '/')
            {
                style = ShortcodeStyle.Close;

                _cursor.Advance();
            }

            // Reach Eof before end of shortcode
            if (_cursor.Eof)
            {
                DiscardCursor();

                return(false);
            }

            SkipWhiteSpace();

            if (!ReadIdentifier())
            {
                DiscardCursor();

                return(false);
            }

            Token identifier = _token;

            SkipWhiteSpace();

            Dictionary <string, string> arguments = null;

            int argumentIndex = 0;

            // Arguments?
            while (true)
            {
                if (ReadString())
                {
                    arguments ??= CreateArgumentsDictionary();

                    arguments[argumentIndex.ToString()] = DecodeString(_token.ToString());

                    argumentIndex += 1;
                }
                else if (ReadIdentifier())
                {
                    var argument = _token;

                    SkipWhiteSpace();

                    // It might just be a value
                    if (ReadEqualSign())
                    {
                        SkipWhiteSpace();

                        if (ReadString())
                        {
                            arguments ??= CreateArgumentsDictionary();

                            arguments[argument.ToString()] = DecodeString(_token.ToString());
                        }
                        else if (ReadValue())
                        {
                            arguments ??= CreateArgumentsDictionary();

                            arguments[argument.ToString()] = _token.ToString();
                        }
                        else
                        {
                            DiscardCursor();

                            return(false);
                        }
                    }
                    else
                    {
                        // Positional argument that looks like an identifier

                        _cursor.Seek(argument.Start);

                        if (ReadValue())
                        {
                            arguments ??= CreateArgumentsDictionary();

                            arguments[argumentIndex.ToString()] = _token.ToString();

                            argumentIndex += 1;
                        }
                        else
                        {
                            _cursor.Seek(argument.Start);

                            break;
                        }
                    }
                }
                else if (ReadValue())
                {
                    arguments ??= CreateArgumentsDictionary();

                    arguments[argumentIndex.ToString()] = _token.ToString();

                    argumentIndex += 1;
                }
                else
                {
                    break;
                }

                SkipWhiteSpace();
            }

            // Is it a self-closing tag?
            if (_cursor.Char == '/' && _cursor.PeekNext() == ']')
            {
                style = ShortcodeStyle.SelfClosing;

                _cursor.Advance();
            }

            // Expect closing bracket
            if (_cursor.Char != ']')
            {
                DiscardCursor();

                return(false);
            }

            // Read all ']' so we can detect escaped tags
            do
            {
                closeBraces += 1;
                _cursor.Advance();
            } while (_cursor.Char == ']');

            shortcode           = new Shortcode(identifier.ToString(), style, openBraces, closeBraces, index, _cursor.Offset - index - 1);
            shortcode.Arguments = new Arguments(arguments);

            PromoteCursor();

            return(true);

            // Local function to use the same logic to create the arguments dictionary
            Dictionary <string, string> CreateArgumentsDictionary()
            {
                return(new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase));
            }
        }
        private async ValueTask <string> FoldClosingTagsAsync(string input, List <Node> nodes, int index, int length, Context context)
        {
            // This method should not be called when nodes has a single RawText element.
            // It's implementation assumes at least two nodes are provided.

            using var sb = StringBuilderPool.GetInstance();

            // The index of the next shortcode opening node
            var cursor = index;

            // Process the list
            while (cursor <= index + length - 1)
            {
                Shortcode start = null;
                var       head  = 0;
                var       tail  = 0;

                // Find the next opening tag
                while (cursor <= index + length - 1 && start == null)
                {
                    var node = nodes[cursor];

                    if (node is Shortcode shortCode)
                    {
                        if (shortCode.Style == ShortcodeStyle.Open)
                        {
                            head  = cursor;
                            start = shortCode;
                        }
                        else
                        {
                            // These closing tags need to be rendered
                            sb.Builder.Append(input, shortCode.SourceIndex, shortCode.SourceLength + 1);
                        }
                    }
                    else
                    {
                        var text = node as RawText;

                        sb.Builder.Append(text.Buffer, text.Offset, text.Count);
                    }

                    cursor += 1;
                }

                // if start is null, then there is nothing to fold
                if (start == null)
                {
                    return(sb.Builder.ToString());
                }

                Shortcode end = null;

                var depth = 1;

                // Find a matching closing tag
                while (cursor <= index + length - 1 && end == null)
                {
                    if (nodes[cursor] is Shortcode shortCode)
                    {
                        if (String.Equals(start.Identifier, shortCode.Identifier, StringComparison.OrdinalIgnoreCase))
                        {
                            if (shortCode.Style == ShortcodeStyle.Open)
                            {
                                // We need to count all opening shortcodes matching the start to account for:
                                // [a] [a] [/a] [/a]

                                depth += 1;
                            }
                            else
                            {
                                depth -= 1;

                                if (depth == 0)
                                {
                                    tail = cursor;
                                    end  = shortCode;
                                }
                            }
                        }
                    }

                    cursor += 1;
                }

                // Is it a single tag?
                if (end == null)
                {
                    cursor = head + 1;

                    // If there are more than one open/close brace we don't evaluate the shortcode
                    if (start.OpenBraces > 1 || start.CloseBraces > 1)
                    {
                        // We need to escape the braces if counts match
                        var bracesToSkip = start.OpenBraces == start.CloseBraces ? 1 : 0;

                        sb.Builder.Append('[', start.OpenBraces - bracesToSkip);
                        sb.Builder.Append(input, start.SourceIndex + start.OpenBraces, start.SourceLength - start.CloseBraces - start.OpenBraces + 1);
                        sb.Builder.Append(']', start.CloseBraces - bracesToSkip);
                    }
                    else
                    {
                        await AppendAsync(sb.Builder, input, start, null, context);
                    }
                }
                else
                {
                    // Standard braces are made of 1 brace on each edge
                    var standardBraces = start.OpenBraces == 1 && start.CloseBraces == 1 && end.OpenBraces == 1 && end.CloseBraces == 1;
                    var balancedBraces = start.OpenBraces == end.CloseBraces && start.CloseBraces == end.OpenBraces;

                    if (standardBraces)
                    {
                        // Are the tags adjacent?
                        if (tail - head == 1)
                        {
                            start.Content = "";
                            await AppendAsync(sb.Builder, input, start, end, context);
                        }
                        // Is there a single node between the tags?
                        else if (tail - head == 2)
                        {
                            // Render the inner node (raw or shortcode)
                            var content = nodes[head + 1];

                            // Set it to the start shortcode
                            using (var sbContent = StringBuilderPool.GetInstance())
                            {
                                await AppendAsync(sbContent.Builder, input, content, null, context);

                                start.Content = sbContent.ToString();
                            }

                            // Render the start shortcode
                            await AppendAsync(sb.Builder, input, start, end, context);
                        }
                        // Fold the inner nodes
                        else
                        {
                            start.Content = await FoldClosingTagsAsync(input, nodes, head + 1, tail - head - 1, context);
                            await AppendAsync(sb.Builder, input, start, end, context);
                        }
                    }
                    else
                    {
                        // Balanced braces represent an escape sequence, e.g. [[upper]foo[/upper]] -> [upper]foo[/upper]
                        if (balancedBraces)
                        {
                            var bracesToSkip = start.OpenBraces == end.CloseBraces ? 1 : 0;

                            sb.Builder.Append('[', start.OpenBraces - bracesToSkip);
                            sb.Builder.Append(input, start.SourceIndex + start.OpenBraces, end.SourceIndex + end.SourceLength - end.CloseBraces - start.SourceIndex - start.OpenBraces + 1);
                            sb.Builder.Append(']', end.CloseBraces - bracesToSkip);
                        }
                        // Unbalanced braces only evaluate inner content, e.g. [upper]foo[/upper]]
                        else
                        {
                            // Are the tags adjacent?
                            if (tail - head == 1)
                            {
                                AppendRawNode(sb.Builder, input, start);
                                AppendRawNode(sb.Builder, input, end);
                            }
                            // Is there a single node between the tags?
                            else if (tail - head == 2)
                            {
                                // Render the inner node (raw or shortcode)
                                var content = nodes[head + 1];

                                AppendRawNode(sb.Builder, input, start);
                                await AppendAsync(sb.Builder, input, content, null, context);

                                AppendRawNode(sb.Builder, input, end);
                            }
                            // Fold the inner nodes
                            else
                            {
                                var content = await FoldClosingTagsAsync(input, nodes, head + 1, tail - head - 1, context);

                                AppendRawNode(sb.Builder, input, start);
                                sb.Builder.Append(content);
                                AppendRawNode(sb.Builder, input, end);
                            }
                        }
                    }
                }
            }

            return(sb.Builder.ToString());
        }
        private async Task AppendAsync(StringBuilder builder, string source, Node start, Shortcode end, Context context)
        {
            switch (start)
            {
            case RawText raw:
                builder.Append(raw.Buffer, raw.Offset, raw.Count);
                return;

            case Shortcode code:
                foreach (var provider in Providers)
                {
                    var result = await provider.EvaluateAsync(code.Identifier, code.Arguments, code.Content, context);

                    if (result != null)
                    {
                        builder.Append(result);
                        return;
                    }
                }

                // Return original content if no handler is found
                if (end == null)
                {
                    // No closing tag
                    builder.Append(source, code.SourceIndex, code.SourceLength + code.CloseBraces);
                }
                else
                {
                    builder
                    .Append(source, code.SourceIndex, code.SourceLength + code.CloseBraces)
                    .Append(code.Content)
                    .Append(source, end.SourceIndex, end.SourceLength + end.CloseBraces)
                    ;
                }

                break;

            default:
                throw new NotSupportedException();
            }
        }
示例#7
0
        private async ValueTask <string> FoldClosingTagsAsync(string input, List <Node> nodes, int index, int length, Context context)
        {
            // This method should not be called when nodes has a single RawText element.
            // It's implementation assumes at least two nodes are provided.

            using var sb = StringBuilderPool.GetInstance();

            // The index of the next shortcode opening node
            var cursor = index;

            // Process the list
            while (cursor <= index + length - 1)
            {
                Shortcode start = null;
                var       head  = 0;
                var       tail  = 0;

                // Find the next opening tag
                while (cursor < nodes.Count && start == null)
                {
                    var node = nodes[cursor];

                    if (node is Shortcode shortCode)
                    {
                        if (shortCode.Style == ShortcodeStyle.Open)
                        {
                            head  = cursor;
                            start = shortCode;
                        }
                    }
                    else
                    {
                        var text = node as RawText;

                        sb.Builder.Append(text.Text);
                    }

                    cursor += 1;
                }

                // if start is null, then there is nothing to fold
                if (start == null)
                {
                    return(sb.Builder.ToString());
                }

                Shortcode end = null;

                var depth = 1;

                // Find a matching closing tag
                while (cursor <= index + length - 1 && end == null)
                {
                    if (nodes[cursor] is Shortcode shortCode)
                    {
                        if (String.Equals(start.Identifier, shortCode.Identifier, StringComparison.OrdinalIgnoreCase))
                        {
                            if (shortCode.Style == ShortcodeStyle.Open)
                            {
                                // We need to count all opening shortcodes matching the start to account for:
                                // [a] [a] [/a] [/a]

                                depth += 1;
                            }
                            else
                            {
                                depth -= 1;

                                if (depth == 0)
                                {
                                    tail = cursor;
                                    end  = shortCode;
                                }
                            }
                        }
                    }

                    cursor += 1;
                }

                // Is is a single tag?
                if (end == null)
                {
                    cursor = head + 1;

                    // If there are more than one open/close brace we don't evaluate the shortcode
                    if (start.OpenBraces > 1 || start.CloseBraces > 1)
                    {
                        // We need to escape the braces if counts match
                        var bracesToSkip = start.OpenBraces == start.CloseBraces ? 1 : 0;

                        sb.Builder.Append('[', start.OpenBraces - bracesToSkip);
                        sb.Builder.Append(input.Substring(start.SourceIndex + start.OpenBraces, start.SourceLength - start.CloseBraces - start.OpenBraces + 1));
                        sb.Builder.Append(']', start.CloseBraces - bracesToSkip);
                    }
                    else
                    {
                        sb.Builder.Append(await RenderAsync(start, context));
                    }
                }
                else
                {
                    // If the braces are unbalanced we can't render the shortcode
                    var canRenderShortcode = start.OpenBraces == 1 && start.CloseBraces == 1 && end.OpenBraces == 1 && end.CloseBraces == 1;

                    if (canRenderShortcode)
                    {
                        // Are the tags adjacent?
                        if (tail - head == 1)
                        {
                            start.Content = "";
                            sb.Builder.Append(await RenderAsync(start, context));
                        }
                        // Is there a single Raw text between the tags?
                        else if (tail - head == 2)
                        {
                            var content = nodes[head + 1] as RawText;
                            start.Content = content.Text;
                            sb.Builder.Append(await RenderAsync(start, context));
                        }
                        // Fold the inner nodes
                        else
                        {
                            var content = await FoldClosingTagsAsync(input, nodes, head + 1, tail - head - 1, context);

                            start.Content = content;
                            sb.Builder.Append(await RenderAsync(start, context));
                        }
                    }
                    else
                    {
                        var bracesToSkip = start.OpenBraces == end.CloseBraces ? 1 : 0;

                        sb.Builder.Append('[', start.OpenBraces - bracesToSkip);
                        sb.Builder.Append(input.Substring(start.SourceIndex + start.OpenBraces, end.SourceIndex + end.SourceLength - end.CloseBraces - start.SourceIndex - start.OpenBraces + 1));
                        sb.Builder.Append(']', end.CloseBraces - bracesToSkip);
                    }
                }
            }

            return(sb.Builder.ToString());
        }