Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skipping unknown shortcodes #13

Merged
merged 7 commits into from
Dec 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Shortcodes.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{46FB729C
.github\workflows\publish.yml = .github\workflows\publish.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shortcodes.Benchmarks", "tests\Shortcodes.Benchmarks\Shortcodes.Benchmarks.csproj", "{D7B1F710-04EC-49F1-BE79-FC4A83749E69}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -29,6 +31,10 @@ Global
{EDD2509B-3D55-4EA4-83B3-017462DC090F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDD2509B-3D55-4EA4-83B3-017462DC090F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDD2509B-3D55-4EA4-83B3-017462DC090F}.Release|Any CPU.Build.0 = Release|Any CPU
{D7B1F710-04EC-49F1-BE79-FC4A83749E69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7B1F710-04EC-49F1-BE79-FC4A83749E69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7B1F710-04EC-49F1-BE79-FC4A83749E69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7B1F710-04EC-49F1-BE79-FC4A83749E69}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
128 changes: 101 additions & 27 deletions src/Shortcodes/ShortcodesProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ public async ValueTask<string> EvaluateAsync(string input, Context context = nul
return input;
}

if (context == null)
{
context = new Context();
}

// Don't do anything if brackets can't be found in the input text
var openIndex = input.IndexOf("[", 0, StringComparison.OrdinalIgnoreCase);
var closeIndex = input.IndexOf("]", 0, StringComparison.OrdinalIgnoreCase);
Expand All @@ -54,6 +49,11 @@ public async ValueTask<string> EvaluateAsync(string input, Context context = nul
return input;
}

if (context == null)
{
context = new Context();
}

// Scan for tags
var scanner = new Scanner(input);
var nodes = scanner.Scan();
Expand All @@ -79,7 +79,7 @@ private async ValueTask<string> FoldClosingTagsAsync(string input, List<Node> no
var tail = 0;

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

Expand All @@ -90,6 +90,11 @@ private async ValueTask<string> FoldClosingTagsAsync(string input, List<Node> no
head = cursor;
start = shortCode;
}
else
{
// These closing tags need to be rendered
sb.Builder.Append(input.Substring(shortCode.SourceIndex, shortCode.SourceLength + 1));
}
}
else
{
Expand Down Expand Up @@ -141,7 +146,7 @@ private async ValueTask<string> FoldClosingTagsAsync(string input, List<Node> no
cursor += 1;
}

// Is is a single tag?
// Is it a single tag?
if (end == null)
{
cursor = head + 1;
Expand All @@ -158,54 +163,105 @@ private async ValueTask<string> FoldClosingTagsAsync(string input, List<Node> no
}
else
{
sb.Builder.Append(await RenderAsync(start, context));
sb.Builder.Append(await RenderAsync(input, start, null, 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;
// 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 (canRenderShortcode)
if (standardBraces)
{
// Are the tags adjacent?
if (tail - head == 1)
{
start.Content = "";
sb.Builder.Append(await RenderAsync(start, context));
sb.Builder.Append(await RenderAsync(input, start, end, context));
}
// Is there a single Raw text between the tags?
// Is there a single node 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));
// Render the inner node (raw or shortcode)
var content = nodes[head + 1];

// Set it to the start shortcode
start.Content = await RenderAsync(input, content, null, context);

// Render the start shortcode
sb.Builder.Append(await RenderAsync(input, start, end, 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));
}
sb.Builder.Append(await RenderAsync(input, start, end, context));
}
}
else
{
var bracesToSkip = start.OpenBraces == end.CloseBraces ? 1 : 0;
// 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.Substring(start.SourceIndex + start.OpenBraces, end.SourceIndex + end.SourceLength - end.CloseBraces - start.SourceIndex - start.OpenBraces + 1));
sb.Builder.Append(']', end.CloseBraces - bracesToSkip);
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);
}
// Unbalanced braces only evaluate inner content, e.g. [upper]foo[/upper]]
else
{
// Are the tags adjacent?
if (tail - head == 1)
{
sb.Builder.Append(GetRawNode(input, start));
sb.Builder.Append(GetRawNode(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];

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

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

return sb.Builder.ToString();
}

public async ValueTask<string> RenderAsync(Node node, Context context)
private string GetRawNode(string source, Shortcode node)
{
switch (node)
if (node.OpenBraces == node.CloseBraces)
{
return source.Substring(node.SourceIndex, node.SourceLength + node.CloseBraces);
}
else
{
return source.Substring(node.SourceIndex, node.SourceLength + 1);
}
}

private async Task<string> RenderAsync(string source, Node start, Shortcode end, Context context)
{
switch (start)
{
case RawText raw:
return raw.Text;
Expand All @@ -220,10 +276,28 @@ public async ValueTask<string> RenderAsync(Node node, Context context)
return result;
}
}
break;
}

return "";
// Return original content if no handler is found
if (end == null)
{
// No closing tag
return source.Substring(code.SourceIndex, code.SourceLength + code.CloseBraces);
}
else
{
// Potential optimizations:
// - use a shared argument array to return a list of strings
// - use a lambda argument to execute an action on each string

return source.Substring(code.SourceIndex, code.SourceLength + code.CloseBraces)
+ code.Content
+ source.Substring(end.SourceIndex, end.SourceLength + end.CloseBraces)
;
}

default:
throw new NotSupportedException();
}
}
}
}
12 changes: 12 additions & 0 deletions tests/Shortcodes.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;

namespace Shortcodes.Benchmarks
{
class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<RenderBenchmarks>();
}
}
}
37 changes: 37 additions & 0 deletions tests/Shortcodes.Benchmarks/RenderBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using BenchmarkDotNet.Attributes;
using System.Threading.Tasks;

namespace Shortcodes.Benchmarks
{
[MemoryDiagnoser]
[ShortRunJob]
public class RenderBenchmarks
{
private NamedShortcodeProvider _provider;

private ShortcodesProcessor _processor;

public RenderBenchmarks()
{
_provider = new NamedShortcodeProvider
{
["upper"] = (args, content, ctx) => new ValueTask<string>(content.ToUpperInvariant()),
};

_processor = new ShortcodesProcessor(_provider);
}

[Benchmark]
public async Task<string> Nop() => await _processor.EvaluateAsync("Lorem ipsum dolor est");

[Benchmark]
public async Task<string> Upper() => await _processor.EvaluateAsync("Lorem [upper]ipsum[/upper] dolor est");

[Benchmark]
public async Task<string> Unkown() => await _processor.EvaluateAsync("Lorem [lower]ipsum[/lower] dolor est");

[Benchmark]
public async Task<string> Big() => await _processor.EvaluateAsync("Lorem [upper]ipsum[/upper] dolor est Lorem [upper] Lorem [upper]ipsum[/upper] dolor est [/upper] dolor est Lorem [upper]ipsum[/upper] dolor est Lorem [upper] Lorem [upper]ipsum[/upper] dolor est [/upper] dolor est Lorem ipsum dolor est Lorem ipsum dolor est Lorem ipsum dolor est Lorem ipsum dolor est Lorem ipsum dolor est Lorem ipsum dolor est Lorem ipsum dolor est Lorem ipsum dolor est ");

}
}
16 changes: 16 additions & 0 deletions tests/Shortcodes.Benchmarks/Shortcodes.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Shortcodes\Shortcodes.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions tests/Shortcodes.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,37 @@ public async Task IgnoresIncompleteShortcodes(string input, string expected)
Assert.Equal(expected, await parser.EvaluateAsync(input));
}

[Theory]
[InlineData("[hello][foo]", "Hello world![foo]")]
[InlineData("[hello ][foo]", "Hello world![foo]")]
[InlineData("[foo][hello]", "[foo]Hello world!")]
[InlineData("[upper][foo][/upper]", "[FOO]")]
[InlineData("[upper] [ foo ] [/upper]", " [ FOO ] ")]
[InlineData("[[upper] [[] foo []] [/upper]]", "[upper] [[] foo []] [/upper]")]
[InlineData("[[[upper] [[] foo []] [/upper]]]", "[[upper] [[] foo []] [/upper]]")]
[InlineData("[upper] [hello] [/upper]", " HELLO WORLD! ")]
[InlineData("[foo arg1=blah] ", "[foo arg1=blah] ")]
[InlineData("[ foo arg1=blah ]", "[ foo arg1=blah ]")]
[InlineData("[ foo arg1=blah ] [/foo]", "[ foo arg1=blah ] [/foo]")]
[InlineData("[/foo]", "[/foo]")]
[InlineData(" [/ foo ] ", " [/ foo ] ")]
[InlineData(" [a] [/a] ", " [a] [/a] ")]
[InlineData("[a][hello][/a]", "[a]Hello world![/a]")]
[InlineData("[/a][a][hello][a][/a]", "[/a][a]Hello world![a][/a]")]
[InlineData(" [a][hello][/a] ", " [a]Hello world![/a] ")]
[InlineData(" [a] [hello] [/a] ", " [a] Hello world! [/a] ")]
[InlineData(" [a] [hello] [/a][/a] ", " [a] Hello world! [/a][/a] ")]
[InlineData(" [a]]] [hello] [/a][[/a] ", " [a]]] Hello world! [/a][[/a] ")]
[InlineData(" [a]]] [/a]"," [a]]] [/a]")]
[InlineData("[a]]][/a]","[a]]][/a]")]
[InlineData(" [a]]] [hello]"," [a]]] Hello world!")]
public async Task IngoreUnknownShortcodes(string input, string expected)
{
var parser = new ShortcodesProcessor(_provider);

Assert.Equal(expected, await parser.EvaluateAsync(input));
}

[Theory]
[InlineData("[[hello]]", "[hello]")]
[InlineData("[[[hello]]]", "[[hello]]")]
Expand Down
2 changes: 1 addition & 1 deletion tests/Shortcodes.Tests/Shortcodes.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>

<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down