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); } }
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); }
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);
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(); } }
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()); }