diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md new file mode 100644 index 000000000..1060d0b7f --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md @@ -0,0 +1,16 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update) +AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK 9.0.306 + [Host] : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + + +``` +| Method | TextSizeMultiplier | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------- |------------------- |-------------:|------------:|------------:|------------:|-----------:|----------:|--------------:| +| **Parse** | **1** | **294.4 μs** | **5.65 μs** | **5.55 μs** | **124.5117** | **2.4414** | **-** | **1017.81 KB** | +| **Parse** | **10** | **3,172.7 μs** | **10.81 μs** | **9.59 μs** | **1363.2813** | **187.5000** | **11.7188** | **11146.04 KB** | +| **Parse** | **100** | **36,115.6 μs** | **120.37 μs** | **100.52 μs** | **12333.3333** | **333.3333** | **133.3333** | **101203 KB** | +| **Parse** | **1000** | **335,972.8 μs** | **6,497.19 μs** | **7,482.17 μs** | **127000.0000** | **33000.0000** | **4000.0000** | **1011847.79 KB** | diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv new file mode 100644 index 000000000..291e26d98 --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv @@ -0,0 +1,5 @@ +Method;Job;AnalyzeLaunchVariance;EvaluateOverhead;MaxAbsoluteError;MaxRelativeError;MinInvokeCount;MinIterationTime;OutlierMode;Affinity;EnvironmentVariables;Jit;LargeAddressAware;Platform;PowerPlanMode;Runtime;AllowVeryLargeObjects;Concurrent;CpuGroups;Force;HeapAffinitizeMask;HeapCount;NoAffinitize;RetainVm;Server;Arguments;BuildConfiguration;Clock;EngineFactory;NuGetReferences;Toolchain;IsMutator;InvocationCount;IterationCount;IterationTime;LaunchCount;MaxIterationCount;MaxWarmupIterationCount;MemoryRandomization;MinIterationCount;MinWarmupIterationCount;RunStrategy;UnrollFactor;WarmupCount;TextSizeMultiplier;Mean;Error;StdDev;Gen0;Gen1;Gen2;Allocated +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1;294.4 μs;5.65 μs;5.55 μs;124.5117;2.4414;0.0000;1017.81 KB +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;10;"3,172.7 μs";10.81 μs;9.59 μs;1363.2813;187.5000;11.7188;11146.04 KB +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;100;"36,115.6 μs";120.37 μs;100.52 μs;12333.3333;333.3333;133.3333;101203 KB +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1000;"335,972.8 μs";"6,497.19 μs";"7,482.17 μs";127000.0000;33000.0000;4000.0000;1011847.79 KB diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html new file mode 100644 index 000000000..53e5791ab --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html @@ -0,0 +1,33 @@ + + + + +BenchmarkMarkdown.MarkdownParserBenchmark-20251115-183056 + + + + +

+BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update)
+AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 9.0.306
+  [Host]     : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+  DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+
+
+ + + + + + + + +
MethodTextSizeMultiplierMean ErrorStdDevGen0 Gen1Gen2Allocated
Parse1294.4 μs5.65 μs5.55 μs124.51172.4414-1017.81 KB
Parse103,172.7 μs10.81 μs9.59 μs1363.2813187.500011.718811146.04 KB
Parse10036,115.6 μs120.37 μs100.52 μs12333.3333333.3333133.3333101203 KB
Parse1000335,972.8 μs6,497.19 μs7,482.17 μs127000.000033000.00004000.00001011847.79 KB
+ + diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md new file mode 100644 index 000000000..f186405f0 --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md @@ -0,0 +1,16 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update) +AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK 9.0.306 + [Host] : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + + +``` +| Method | TextSizeMultiplier | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------- |------------------- |-------------:|-------------:|-------------:|------------:|-----------:|----------:|-----------:| +| **Render** | **1** | **500.1 μs** | **9.93 μs** | **17.13 μs** | **150.3906** | **2.9297** | **-** | **1.2 MB** | +| **Render** | **10** | **5,322.4 μs** | **105.85 μs** | **273.24 μs** | **1492.1875** | **195.3125** | **-** | **11.95 MB** | +| **Render** | **100** | **56,038.2 μs** | **1,117.32 μs** | **1,566.33 μs** | **14900.0000** | **500.0000** | **200.0000** | **119.28 MB** | +| **Render** | **1000** | **576,250.8 μs** | **11,258.63 μs** | **11,561.78 μs** | **149000.0000** | **31000.0000** | **4000.0000** | **1192.54 MB** | diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv new file mode 100644 index 000000000..956d9f60f --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv @@ -0,0 +1,5 @@ +Method;Job;AnalyzeLaunchVariance;EvaluateOverhead;MaxAbsoluteError;MaxRelativeError;MinInvokeCount;MinIterationTime;OutlierMode;Affinity;EnvironmentVariables;Jit;LargeAddressAware;Platform;PowerPlanMode;Runtime;AllowVeryLargeObjects;Concurrent;CpuGroups;Force;HeapAffinitizeMask;HeapCount;NoAffinitize;RetainVm;Server;Arguments;BuildConfiguration;Clock;EngineFactory;NuGetReferences;Toolchain;IsMutator;InvocationCount;IterationCount;IterationTime;LaunchCount;MaxIterationCount;MaxWarmupIterationCount;MemoryRandomization;MinIterationCount;MinWarmupIterationCount;RunStrategy;UnrollFactor;WarmupCount;TextSizeMultiplier;Mean;Error;StdDev;Gen0;Gen1;Gen2;Allocated +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1;500.1 μs;9.93 μs;17.13 μs;150.3906;2.9297;0.0000;1.2 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;10;"5,322.4 μs";105.85 μs;273.24 μs;1492.1875;195.3125;0.0000;11.95 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;100;"56,038.2 μs";"1,117.32 μs";"1,566.33 μs";14900.0000;500.0000;200.0000;119.28 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1000;"576,250.8 μs";"11,258.63 μs";"11,561.78 μs";149000.0000;31000.0000;4000.0000;1192.54 MB diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html new file mode 100644 index 000000000..17cd9cb08 --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html @@ -0,0 +1,33 @@ + + + + +BenchmarkMarkdown.MdBenchmark-20251130-183500 + + + + +

+BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update)
+AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 9.0.306
+  [Host]     : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+  DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+
+
+ + + + + + + + +
MethodTextSizeMultiplierMean Error StdDevGen0 Gen1Gen2Allocated
Render1500.1 μs9.93 μs17.13 μs150.39062.9297-1.2 MB
Render105,322.4 μs105.85 μs273.24 μs1492.1875195.3125-11.95 MB
Render10056,038.2 μs1,117.32 μs1,566.33 μs14900.0000500.0000200.0000119.28 MB
Render1000576,250.8 μs11,258.63 μs11,561.78 μs149000.000031000.00004000.00001192.54 MB
+ + diff --git a/cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj b/cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj new file mode 100644 index 000000000..2fda6a3da --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/cs/BenchmarkMarkdown/InputData b/cs/BenchmarkMarkdown/InputData new file mode 100644 index 000000000..ea848ff40 --- /dev/null +++ b/cs/BenchmarkMarkdown/InputData @@ -0,0 +1,56 @@ +# Introduction to Markdown + +Markdown is a lightweight markup language __created__ by John Gruber, with contributions from Aaron Swartz. It’s designed to be easy to read and write, using plain text conventions that translate to valid HTML. + +Markdown allows you to write __bold__ and _italic_ text, create # headings, lists, links, and more — all while keeping the text legible in a raw form. + +--- + +## Basic Formatting + +Here is an example of __bold text__ and _italic text_ in Markdown. Double underscores surround bold text, while single underscores can be used for italics. Like this: + +__This is bold__, _this is italic_, and __this is __bold with some inner _italic_ text__ too__. + +--- + +## Combining Tags + +You can combine __bold__ and _italic_ like this: ___bold italic___ text. + +# Headers + +Markdown supports headers starting with `#`: + +# Heading level 1 + +## Heading level 2 + +### Heading level 3 + +#### Heading level 4 + +--- + +## Paragraphs and line breaks + +Markdown treats one or more empty lines as paragraph breaks. + +Use two spaces at the end of a line for a line break. +Example here with a break. + +___ + +## Lists + +- __Bold item__ +- _Italic item_ +- Combined __bold and _italic_ item__ + +1. First numbered item +2. Second item with _italic_ +3. Third item with __bold__ + + +[Name Link](https://www.example.com \"Tooltip\") +--- diff --git a/cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs b/cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs new file mode 100644 index 000000000..782f6493b --- /dev/null +++ b/cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs @@ -0,0 +1,31 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Markdown; + +namespace BenchmarkMarkdown; + +[MemoryDiagnoser] +public class MarkdownParserBenchmark +{ + private IParser parser; + private string fileContent; + + [Params(1, 10, 100, 1000)] public int TextSizeMultiplier { get; set; } + + [GlobalSetup] + public void Setup() + { + parser = new MarkdownParser(); + + var baseDir = AppContext.BaseDirectory; + var filePath = Path.Combine(baseDir, "InputData"); + var baseFileContent = File.ReadAllText(filePath, Encoding.UTF8); + fileContent = string.Concat(Enumerable.Repeat(baseFileContent, TextSizeMultiplier)); + } + + [Benchmark] + public void Parse() + { + parser.Parse(fileContent); + } +} \ No newline at end of file diff --git a/cs/BenchmarkMarkdown/MdBenchmark.cs b/cs/BenchmarkMarkdown/MdBenchmark.cs new file mode 100644 index 000000000..2e7af6445 --- /dev/null +++ b/cs/BenchmarkMarkdown/MdBenchmark.cs @@ -0,0 +1,31 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Markdown; + +namespace BenchmarkMarkdown; + +[MemoryDiagnoser] +public class MdBenchmark +{ + private Md md; + private string fileContent; + + [Params(1, 10, 100, 1000)] public int TextSizeMultiplier { get; set; } + + [GlobalSetup] + public void Setup() + { + md = new Md(); + + var baseDir = AppContext.BaseDirectory; + var filePath = Path.Combine(baseDir, "InputData"); + var baseFileContent = File.ReadAllText(filePath, Encoding.UTF8); + fileContent = string.Concat(Enumerable.Repeat(baseFileContent, TextSizeMultiplier)); + } + + [Benchmark] + public string Render() + { + return md.Render(fileContent); + } +} \ No newline at end of file diff --git a/cs/BenchmarkMarkdown/Program.cs b/cs/BenchmarkMarkdown/Program.cs new file mode 100644 index 000000000..ee1bca1dd --- /dev/null +++ b/cs/BenchmarkMarkdown/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; +using BenchmarkMarkdown; + +BenchmarkRunner.Run(); +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..360133917 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + + + + + + + + + diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..44691ea66 --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,14 @@ +namespace Markdown; + +public class Md +{ + private readonly IParser parser = new MarkdownParser(); + private readonly IRenderer renderer = new HtmlRenderer(); + + public string Render(string markdownText) + { + var tokens = parser.Parse(markdownText); + var result = renderer.Render(tokens, markdownText); + return result; + } +} \ No newline at end of file diff --git a/cs/Markdown/OpenToken.cs b/cs/Markdown/OpenToken.cs new file mode 100644 index 000000000..ae569c1ea --- /dev/null +++ b/cs/Markdown/OpenToken.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public class OpenToken(MarkdownTag openTag, int textStartPosition) +{ + public MarkdownTag OpenTag = openTag; + public int TextStartPosition = textStartPosition; + public readonly List NestedTokens = []; +} \ No newline at end of file diff --git a/cs/Markdown/Parser/IParser.cs b/cs/Markdown/Parser/IParser.cs new file mode 100644 index 000000000..1e5c9afdf --- /dev/null +++ b/cs/Markdown/Parser/IParser.cs @@ -0,0 +1,6 @@ +namespace Markdown; + +public interface IParser +{ + public IEnumerable Parse(string text); +} \ No newline at end of file diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs new file mode 100644 index 000000000..6230e6033 --- /dev/null +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -0,0 +1,223 @@ +namespace Markdown; + +public class MarkdownParser : IParser +{ + private static readonly Dictionary markdownTagsByType = new() + { + { TagType.Header, new HeaderMarkdownTag() }, + { TagType.Italic, new ItalicMarkdownTag() }, + { TagType.Bold, new BoldMarkdownTag() }, + { TagType.Escaping, new EscapingMarkdownTag() }, + { TagType.EndOfLine, new EndOfLineMarkdownTag() }, + { TagType.None, new NoneMarkdownTag() }, + { TagType.Link, new LinkMarkdownTag() } + }; + + private readonly List possibleTags = []; + + public IEnumerable Parse(string text) + { + var result = new List(); + var tokensWithOpenTag = new Stack(); + + OpenToken? openTokenWithEmptyTag = null; + int currentTagLength; + + for (var i = 0; i < text.Length; i += currentTagLength) + { + var currentTag = GetTag(text, i, tokensWithOpenTag); + + switch (currentTag.TagType) + { + case TagType.Link when LinkMarkdownTag.TryProcessTag(text, i, out var resultLinkToken): + currentTagLength = resultLinkToken.lengthTag; + result.Add(resultLinkToken.token); + continue; + case TagType.Link or TagType.None: + currentTagLength = 1; + openTokenWithEmptyTag ??= new OpenToken(markdownTagsByType[TagType.None], i); + continue; + case TagType.EndOfLine when tokensWithOpenTag.Any(a => a.OpenTag.TagType == TagType.Header): + openTokenWithEmptyTag = new OpenToken(markdownTagsByType[TagType.None], i); + break; + } + + if (openTokenWithEmptyTag is not null && currentTag.TagType != TagType.EndOfLine) + { + var token = CreateToken(text, i, openTokenWithEmptyTag); + AddToken(token, tokensWithOpenTag, result); + openTokenWithEmptyTag = null; + } + + if (currentTag.IsPairedTag) + ProcessPairedTag(text, currentTag, tokensWithOpenTag, i, result); + else + ProcessUnpairedTag(text, currentTag, tokensWithOpenTag, i, result); + + currentTagLength = currentTag.TotalTagLength; + } + + result = AddUnfinishedTags(text, openTokenWithEmptyTag, tokensWithOpenTag, result); + + return result; + } + + #region Processing Tags + + private static void ProcessUnpairedTag(string text, MarkdownTag currentTag, Stack tokensWithOpenTag, + int position, List result) + { + if (currentTag.IsPairedTag) + throw new ArgumentException("Unpaired tag was expected, but paired tag was received"); + + var currentTagLength = currentTag.TagText.Length; + + switch (currentTag.TagType) + { + case TagType.Header: + { + var openToken = new OpenToken(currentTag, position + currentTagLength); + tokensWithOpenTag.Push(openToken); + break; + } + case TagType.EndOfLine: + HeaderMarkdownTag.ProcessEndTag(text, tokensWithOpenTag, position, result); + break; + case TagType.Escaping: + { + var startPosition = position + currentTagLength; + var openToken = new OpenToken(currentTag, startPosition); + var token = CreateToken(text, startPosition + 1, openToken); + AddToken(token, tokensWithOpenTag, result); + break; + } + } + } + + private static void ProcessPairedTag(string text, MarkdownTag currentTag, Stack tokensWithOpenTag, + int position, List result) + { + if (!currentTag.IsPairedTag) + throw new ArgumentException("Paired tag was expected, but unpaired tag was received"); + + if (tokensWithOpenTag.IsPeekEqual(currentTag.TagType)) + { + var openToken = tokensWithOpenTag.Pop(); + AddToken(CreateToken(text, position, openToken), tokensWithOpenTag, result); + } + else if (tokensWithOpenTag.All(t => t.OpenTag.TagType != currentTag.TagType)) + { + tokensWithOpenTag.Push(new OpenToken(currentTag, position + currentTag.TagText.Length)); + } + } + + #endregion + + #region BorderlineCases + + private static OpenToken ConvertTagBoldInsideTagItalic(OpenToken openToken) + { + if (openToken.OpenTag.TagType == TagType.Italic && openToken.NestedTokens.Count > 0) + for (var i = 0; i < openToken.NestedTokens.Count; i++) + openToken.NestedTokens[i] = ConvertTagBoldToTagNone(openToken.NestedTokens[i]); + return openToken; + } + + private static Token ConvertTagBoldToTagNone(Token token) + { + if (token.TagType == TagType.Bold) + { + var tagText = markdownTagsByType[TagType.Bold].TagText; + var newText = $"{tagText}{token.Content}{tagText}"; + var children = token.Children?.Select(ConvertTagBoldToTagNone).ToList(); + return new Token(TagType.None, newText, children); + } + + if (token.Children != null) + { + var children = token.Children.Select(ConvertTagBoldToTagNone).ToList(); + return new Token(token.TagType, token.Content, children); + } + + return token; + } + + #endregion + + private static List AddUnfinishedTags(string text, OpenToken? openTokenWithEmptyTag, + Stack tokensWithOpenTag, List listTokens) + { + if (openTokenWithEmptyTag is not null && tokensWithOpenTag.Count == 0) + { + var token = CreateToken(text, text.Length, openTokenWithEmptyTag); + AddToken(token, tokensWithOpenTag, listTokens); + } + + while (tokensWithOpenTag.Count > 0) + { + var openToken = tokensWithOpenTag.Pop(); + if (openToken.OpenTag.TagType != TagType.Header) + { + var token = CreateTokenForPairedTagWithoutPair(text, text.Length, openToken); + AddToken(token, tokensWithOpenTag, listTokens); + } + else + { + var token = CreateToken(text, text.Length, openToken); + AddToken(token, tokensWithOpenTag, listTokens); + } + } + + return listTokens; + } + + public static void AddToken(Token token, Stack tokensWithOpenTag, List result) + { + if (tokensWithOpenTag.Count == 0) + result.Add(token); + else + tokensWithOpenTag.Peek().NestedTokens.Add(token); + } + + public static Token CreateToken(string text, int endPosition, OpenToken openToken) + { + var length = endPosition - openToken.TextStartPosition; + + if (openToken.OpenTag.TagType == TagType.Italic) openToken = ConvertTagBoldInsideTagItalic(openToken); + + if (!markdownTagsByType[openToken.OpenTag.TagType].IsTagCorrect(text, endPosition, openToken)) + { + var tagText = openToken.OpenTag.TagText; + var content = $"{tagText}{text.Substring(openToken.TextStartPosition, length)}{tagText}"; + return new Token(TagType.None, content); + } + + if (openToken.NestedTokens is [{ TagType: TagType.None }] || openToken.NestedTokens.Count == 0) + return new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length)); + + return new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length), + openToken.NestedTokens); + } + + public static TokenTagLink CreateLinkToken(string content, string linkText, string? tooltipText) + { + return new TokenTagLink(content, linkText, tooltipText); + } + + private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosition, OpenToken openToken) + { + var startPosition = openToken.TextStartPosition - openToken.OpenTag.TagText.Length; + return new Token(TagType.None, text.Substring(startPosition, endPosition - startPosition)); + } + + private MarkdownTag GetTag(string text, int position, Stack tokensWithOpenTag) + { + possibleTags.Clear(); + + foreach (var tag in markdownTagsByType.Values) + if (tag.IsStartOfTag(text, position, tokensWithOpenTag.Any(t => t.OpenTag.TagType == tag.TagType))) + possibleTags.Add(tag); + + return possibleTags.OrderByDescending(x => x.TagText.Length).First(); + } +} \ No newline at end of file diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs new file mode 100644 index 000000000..e3eb178f9 --- /dev/null +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -0,0 +1,63 @@ +using System.Text; + +namespace Markdown; + +public class HtmlRenderer : IRenderer +{ + private readonly Dictionary tags = new() + { + { TagType.None, new HtmlTag(false, "") }, + { TagType.Header, new HtmlTag(true, "

", "

") }, + { TagType.Italic, new HtmlTag(true, "", "") }, + { TagType.Bold, new HtmlTag(true, "", "") }, + { TagType.Escaping, new HtmlTag(true, "\\") }, + { TagType.EndOfLine, new HtmlTag(false, "\n") } + }; + + public string Render(IEnumerable tokens, string text) + { + var stringBuilder = new StringBuilder(text.Length); + + foreach (var token in tokens) + { + RenderToken(token, stringBuilder); + } + + return stringBuilder.ToString(); + } + + private void RenderToken(Token token, StringBuilder builder) + { + if (token.TagType == TagType.Link) + { + RenderLinkToken((TokenTagLink)token, builder); + return; + } + + var tag = tags[token.TagType]; + + if (token.Children is null) + { + var content = tag.IsPairedTag + ? $"{tag.StartTag}{token.Content}{tag.EndTag}" + : $"{tag.StartTag}{token.Content}"; + builder.Append(content); + return; + } + + builder.Append(tag.StartTag); + foreach (var child in token.Children) + { + RenderToken(child, builder); + } + builder.Append(tag.EndTag); + } + + private void RenderLinkToken(TokenTagLink token, StringBuilder builder) + { + var content = token.TooltipText is not null + ? $"{LinkHtmlTag.StartLinkTag}{token.LinkText}{LinkHtmlTag.EndLinkTag} {LinkHtmlTag.StartTitleTag}{token.TooltipText}{LinkHtmlTag.EndTitleTag}>{token.Content}{LinkHtmlTag.EndTag}" + : $"{LinkHtmlTag.StartLinkTag}{token.LinkText}{LinkHtmlTag.EndLinkTag}>{token.Content}{LinkHtmlTag.EndTag}"; + builder.Append(content); + } +} \ No newline at end of file diff --git a/cs/Markdown/Renderer/IRenderer.cs b/cs/Markdown/Renderer/IRenderer.cs new file mode 100644 index 000000000..863f65d04 --- /dev/null +++ b/cs/Markdown/Renderer/IRenderer.cs @@ -0,0 +1,6 @@ +namespace Markdown; + +public interface IRenderer +{ + public string Render(IEnumerable tokens, string text); +} \ No newline at end of file diff --git a/cs/Markdown/StackExtensions.cs b/cs/Markdown/StackExtensions.cs new file mode 100644 index 000000000..d11e0e679 --- /dev/null +++ b/cs/Markdown/StackExtensions.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +public static class StackExtensions +{ + public static bool IsPeekEqual(this Stack tokensWithOpenTag, TagType tagType) + { + return tokensWithOpenTag.Count > 0 && tokensWithOpenTag.Peek().OpenTag.TagType == tagType; + } +} diff --git a/cs/Markdown/Tag/HtmlTag/HtmlTag.cs b/cs/Markdown/Tag/HtmlTag/HtmlTag.cs new file mode 100644 index 000000000..5aa7af6ed --- /dev/null +++ b/cs/Markdown/Tag/HtmlTag/HtmlTag.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public class HtmlTag(bool isPairedTag, string startTag, string? endTag = null) +{ + public string StartTag { get; } = startTag; + public string? EndTag { get; } = endTag; + public bool IsPairedTag { get; } = isPairedTag; +} \ No newline at end of file diff --git a/cs/Markdown/Tag/HtmlTag/LinkHtmlTag.cs b/cs/Markdown/Tag/HtmlTag/LinkHtmlTag.cs new file mode 100644 index 000000000..1502444fb --- /dev/null +++ b/cs/Markdown/Tag/HtmlTag/LinkHtmlTag.cs @@ -0,0 +1,11 @@ +namespace Markdown; + +public class LinkHtmlTag() : HtmlTag(true, "") +{ + public static string StartLinkTag = " text.Length) return false; + + var isSatisfiesConditions = false; + if (isSameTagAlreadyOpen) + isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); + else if (text.Length > position + TagText.Length) + isSatisfiesConditions = !char.IsWhiteSpace(text[position + TagText.Length]); + else + isSatisfiesConditions = true; + + return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isSatisfiesConditions; + } + + public override bool IsTagCorrect(string text, int endPosition, OpenToken openToken) + { + if (openToken.OpenTag.TagType != TagType.Bold) + throw new ArgumentException($"{openToken.OpenTag.TagType.ToString()} should be {nameof(TagType.Bold)}"); + + var isTokenEmpty = openToken.TextStartPosition == endPosition && openToken.NestedTokens.Count == 0; + + return !IsTokenLocatedInsideWords(text, endPosition, openToken) && + !IsTokenHighlightsPartOfWordWithDigits(text, endPosition, openToken) && !isTokenEmpty; + } + + private bool IsTokenLocatedInsideWords(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + var textContainsSeveralWords = false; + + var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + foreach (var symbol in part) + if (char.IsWhiteSpace(symbol)) + { + textContainsSeveralWords = true; + break; + } + + return (isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords; + } + + private bool IsTokenHighlightsPartOfWordWithDigits(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + + var isOnlyPartOfWordHighlighted = isStartTagInMiddleWord || isEndTagInMiddleWord; + var partText = text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + return isOnlyPartOfWordHighlighted && partText.All(char.IsLetterOrDigit) && partText.Any(char.IsDigit); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs new file mode 100644 index 000000000..141b209d9 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs @@ -0,0 +1,12 @@ +namespace Markdown; + +public class EndOfLineMarkdownTag() : MarkdownTag(TagType.EndOfLine, Environment.NewLine, false) +{ + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TagText.Length + position > text.Length) return false; + + if (text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal)) return true; + return false; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs new file mode 100644 index 000000000..5f4f1cddb --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs @@ -0,0 +1,15 @@ +namespace Markdown; + +public class EscapingMarkdownTag() : MarkdownTag(TagType.Escaping, "\\", false, 2) +{ + private static readonly List escapeSymbols = ['\\', '#', '_']; + + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TotalTagLength + position > text.Length) return false; + + var isCanEscaping = escapeSymbols.Contains(text[position + 1]); + + return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isCanEscaping; + } +} diff --git a/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs new file mode 100644 index 000000000..928f11fc7 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs @@ -0,0 +1,34 @@ +namespace Markdown; + +public class HeaderMarkdownTag() : MarkdownTag(TagType.Header, "# ", false) +{ + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TagText.Length + position > text.Length) return false; + + var doubleNewLine = string.Concat(Enumerable.Repeat(Environment.NewLine, 2)); + var length = doubleNewLine.Length; + + var isNewParagraph = position == 0 || (position >= length && text.AsSpan(position - length, length) + .Equals(doubleNewLine, StringComparison.Ordinal)); + + return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isNewParagraph && + !isSameTagAlreadyOpen; + } + + public static void ProcessEndTag(string text, Stack tokensWithOpenTag, int position, + List result) + { + if (tokensWithOpenTag.All(t => t.OpenTag.TagType != TagType.Header)) + return; + + while (!tokensWithOpenTag.IsPeekEqual(TagType.Header)) + tokensWithOpenTag.Pop(); + + if (tokensWithOpenTag.Count > 0) + { + var headerToken = tokensWithOpenTag.Pop(); + MarkdownParser.AddToken(MarkdownParser.CreateToken(text, position, headerToken), tokensWithOpenTag, result); + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs new file mode 100644 index 000000000..debc70902 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs @@ -0,0 +1,77 @@ +namespace Markdown; + +public class ItalicMarkdownTag() : MarkdownTag(TagType.Italic, "_", true) +{ + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TagText.Length + position > text.Length) return false; + + var isSatisfiesConditions = false; + if (isSameTagAlreadyOpen) + isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); + else if (text.Length > position + TagText.Length) + isSatisfiesConditions = !char.IsWhiteSpace(text[position + TagText.Length]); + else + isSatisfiesConditions = true; + + var isContainsTagItalic = text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal); + var tagBoldLength = BoldMarkdownTag.TagText.Length; + var tagBoldText = BoldMarkdownTag.TagText; + var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) + .Equals(tagBoldText, StringComparison.Ordinal); + return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; + } + + public override bool IsTagCorrect(string text, int endPosition, OpenToken openToken) + { + if (openToken.OpenTag.TagType != TagType.Italic) + throw new ArgumentException($"{openToken.OpenTag.TagType.ToString()} should be {nameof(TagType.Italic)}"); + + var isTokenEmpty = openToken.TextStartPosition == endPosition && openToken.NestedTokens.Count == 0; + + return !IsTokenLocatedInsideWords(text, endPosition, openToken) && + !IsTokenHighlightsPartOfWordWithDigits(text, endPosition, openToken) && !isTokenEmpty; + } + + private bool IsTokenLocatedInsideWords(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + var textContainsSeveralWords = false; + + var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + foreach (var symbol in part) + if (char.IsWhiteSpace(symbol)) + { + textContainsSeveralWords = true; + break; + } + + return (isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords; + } + + private bool IsTokenHighlightsPartOfWordWithDigits(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + + var isOnlyPartOfWordHighlighted = isStartTagInMiddleWord || isEndTagInMiddleWord; + var partText = text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + return isOnlyPartOfWordHighlighted && partText.All(char.IsLetterOrDigit) && partText.Any(char.IsDigit); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/LinkMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/LinkMarkdownTag.cs new file mode 100644 index 000000000..5dcf62db2 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/LinkMarkdownTag.cs @@ -0,0 +1,88 @@ +namespace Markdown; + +public class LinkMarkdownTag() : MarkdownTag(TagType.Link, "[", true) +{ + public static readonly string StartTagTextForName = "["; + public static readonly string EndTagTextForName = "]"; + public static readonly string StartTagTextForLink = "("; + public static readonly string EndTagTextForLink = ")"; + public static readonly string StartTagTextForTooltip = " \""; + public static readonly string EndTagTextForTooltip = "\""; + + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TagText.Length + position > text.Length) return false; + + var isContainsStartTagTextForName = text.AsSpan(position, StartTagTextForName.Length).Equals(StartTagTextForName, StringComparison.Ordinal); + + return isContainsStartTagTextForName; + } + + public static bool TryProcessTag(string text, int startPosition, out (TokenTagLink? token, int lengthTag) result) + { + var currentPosition = startPosition; + + if (!text.StartsWith(StartTagTextForName, StringComparison.Ordinal)) + { + result = (null, 0); + return false; + } + + var indexEndTagTextForName = text.IndexOf(EndTagTextForName, startPosition + StartTagTextForName.Length, StringComparison.Ordinal); + var indexStartTagTextForLink = text.IndexOf(StartTagTextForLink, indexEndTagTextForName + EndTagTextForName.Length, StringComparison.Ordinal); + + var indexStartTagTextForTooltip = -1; + var indexEndTagTextForTooltip = -1; + + if (TryFindTooltip(text, currentPosition, out var tooltip)) + { + indexStartTagTextForTooltip = tooltip.start; + indexEndTagTextForTooltip = tooltip.end; + } + + var indexEndTagTextForLink = text.IndexOf(EndTagTextForLink, indexStartTagTextForLink + StartTagTextForLink.Length, StringComparison.Ordinal); + + if (indexEndTagTextForName == -1 || indexStartTagTextForLink == -1 || indexEndTagTextForLink == -1) + { + result = (null, 0); + return false; + } + + string linkText; + string? tooltipText = null; + + if (indexStartTagTextForTooltip == -1) + { + linkText = text.Substring(indexStartTagTextForLink + StartTagTextForLink.Length, + indexEndTagTextForLink - indexStartTagTextForLink - StartTagTextForLink.Length); + } + else + { + linkText = text.Substring(indexStartTagTextForLink + StartTagTextForLink.Length, + indexStartTagTextForTooltip - indexStartTagTextForLink - StartTagTextForLink.Length); + tooltipText = text.Substring(indexStartTagTextForTooltip + StartTagTextForTooltip.Length, + indexEndTagTextForTooltip - indexStartTagTextForTooltip - StartTagTextForTooltip.Length); + } + + + var content = text.Substring(startPosition + StartTagTextForName.Length, indexEndTagTextForName - startPosition - EndTagTextForName.Length); + + result = (MarkdownParser.CreateLinkToken(content, linkText, tooltipText), indexEndTagTextForLink - startPosition + 1); + return true; + } + + private static bool TryFindTooltip(string text, int startPosition, out (int start, int end) positions) + { + var indexStartTagTextForTooltip = text.IndexOf(StartTagTextForTooltip, startPosition, StringComparison.Ordinal); + var indexEndTagTextForTooltip = text.IndexOf(EndTagTextForTooltip, indexStartTagTextForTooltip + StartTagTextForTooltip.Length, StringComparison.Ordinal); + + if (indexEndTagTextForTooltip == -1 || indexStartTagTextForTooltip == -1) + { + positions = (0, 0); + return false; + } + + positions = (indexStartTagTextForTooltip, indexEndTagTextForTooltip); + return true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs new file mode 100644 index 000000000..261d82f48 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs @@ -0,0 +1,16 @@ +namespace Markdown; + +public abstract class MarkdownTag(TagType tagType, string tagText, bool isPairedTag, int? tagLength = null) +{ + public TagType TagType { get; } = tagType; + public string TagText { get; } = tagText; + public bool IsPairedTag { get; } = isPairedTag; + public int TotalTagLength { get; } = tagLength ?? tagText.Length; + + public abstract bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen); + + public virtual bool IsTagCorrect(string text, int endPosition, OpenToken openToken) + { + return true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs new file mode 100644 index 000000000..65bbbe930 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +public class NoneMarkdownTag() : MarkdownTag(TagType.None, "", false) +{ + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) + { + return true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/TagType.cs b/cs/Markdown/Tag/TagType.cs new file mode 100644 index 000000000..90309fefa --- /dev/null +++ b/cs/Markdown/Tag/TagType.cs @@ -0,0 +1,12 @@ +namespace Markdown; + +public enum TagType +{ + None, + Header, + Italic, + Bold, + Escaping, + Link, + EndOfLine +} \ No newline at end of file diff --git a/cs/Markdown/Tests/HtmlRendererTests.cs b/cs/Markdown/Tests/HtmlRendererTests.cs new file mode 100644 index 000000000..d86cca1c4 --- /dev/null +++ b/cs/Markdown/Tests/HtmlRendererTests.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown.Tests; + +[TestFixture] +public class HtmlRendererTests +{ + private IRenderer renderer; + + [SetUp] + public void SetUp() + { + renderer = new HtmlRenderer(); + } + + [TestCaseSource(nameof(CasesWhenListWithOneToken))] + [Description("Checks each tag")] + public void Parse_ReturnsString_WhenListWithOneToken(IEnumerable inputTokens, string inputText, string expectedResult) + { + var result = renderer.Render(inputTokens, inputText); + + result.Should().BeEquivalentTo(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenListWithMultipleNestedTokens))] + public void Parse_ReturnsString_WhenListWithMultipleNestedTokens(IEnumerable inputTokens, string inputText, string expectedResult) + { + var result = renderer.Render(inputTokens, inputText); + + result.Should().BeEquivalentTo(expectedResult); + } + + public static IEnumerable CasesWhenListWithOneToken() + { + yield return new TestCaseData(new List { new (TagType.None, "Human") }, "Human", "Human"); + yield return new TestCaseData(new List { new (TagType.Italic, "Human") }, "Human", "Human"); + yield return new TestCaseData(new List { new (TagType.Bold, "Human") }, "Human", "Human"); + yield return new TestCaseData(new List { new (TagType.Escaping, "\\") }, "Human", @"\\"); + yield return new TestCaseData(new List { new (TagType.Header, "Human") }, "Human", "

Human

"); + } + + public static IEnumerable CasesWhenListWithMultipleNestedTokens() + { + yield return new TestCaseData(new List { new (TagType.Header, "Human1 Human2", [ + new Token(TagType.Bold, "Human1"), + new Token(TagType.None, " "), + new Token(TagType.Italic, "Human2") + ]) }, "Human1 Human2", "

Human1 Human2

"); + + yield return new TestCaseData(new List { new (TagType.Header, "Human1 Human2", new List() + { + new (TagType.Bold, "Human1"), + new (TagType.None, " "), + new (TagType.Italic, "Human2"), + }) }, "Human1 Human2", "

Human1 Human2

"); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tests/MarkdownParserTests.cs b/cs/Markdown/Tests/MarkdownParserTests.cs new file mode 100644 index 000000000..c1a9a6590 --- /dev/null +++ b/cs/Markdown/Tests/MarkdownParserTests.cs @@ -0,0 +1,314 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown.Tests; + +[TestFixture] +public class MarkdownParserTests +{ + private static readonly Dictionary pairedTags = new() + { + { TagType.Italic, "_" }, + { TagType.Bold, "__" } + }; + + private IParser parser; + + [SetUp] + public void SetUp() + { + parser = new MarkdownParser(); + } + + [TestCase("wordA wordB")] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithoutTags(string input) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo([new Token(TagType.None, "wordA wordB")]); + } + + [TestCaseSource(nameof(CasesWhenTextWithOnePairedTag))] + [Description("Checks each paired tag")] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithOnePairedTag(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNonNestedPairedTags(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNestedPairedTags(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithPairedTagWithoutPair))] + [Description("Checks each paired tag")] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithPairedTagWithoutPair(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] + public void Parse_ReturnsIEnumerableTokens_WhenEmptyTextInsideTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesTextContainsHeaderTag))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsHeaderTag(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsEscapingTag(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsOverlappingTags(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] + public void Parse_ReturnsIEnumerableTokens_WhenBoldTagInsideItalicTag(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] + public void Parse_ReturnsIEnumerableTokens_WhenItalicTagInsideBoldTag(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithNumbersAndContainsBoldItalicTags(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsBoldItalicTagsInMiddleWords(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsLinkTag))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsLinkTag(string input, + IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + public static IEnumerable CasesWhenTextWithOnePairedTag() + { + foreach (var (tagType, tagContent) in pairedTags) + { + yield return new TestCaseData($"{tagContent}wordA wordB{tagContent}", + new[] { new Token(tagType, "wordA wordB") }); + yield return new TestCaseData($"wordA {tagContent}wordB wordC{tagContent} wordD", + new[] + { + new Token(TagType.None, "wordA "), new Token(tagType, "wordB wordC"), + new Token(TagType.None, " wordD") + }); + } + } + + public static IEnumerable CasesWhenTextWithMultipleNonNestedPairedTags() + { + yield return new TestCaseData("_wordA_ __wordB__ wordC", + new[] + { + new Token(TagType.Italic, "wordA"), new Token(TagType.None, " "), new Token(TagType.Bold, "wordB"), + new Token(TagType.None, " wordC") + }); + } + + public static IEnumerable CasesWhenTextWithMultipleNestedPairedTags() + { + yield return new TestCaseData("__wordA _wordB_ wordC__", + new[] + { + new Token(TagType.Bold, "wordA _wordB_ wordC", + [ + new Token(TagType.None, "wordA "), new Token(TagType.Italic, "wordB"), + new Token(TagType.None, " wordC") + ]) + }); + } + + public static IEnumerable CasesWhenTextWithPairedTagWithoutPair() + { + foreach (var (tagType, tagContent) in pairedTags) + yield return new TestCaseData($"{tagContent}wordA", + new[] { new Token(TagType.None, $"{tagContent}wordA") }); + } + + public static IEnumerable CasesWhenEmptyTextInsideTags() + { + yield return new TestCaseData("__", new[] { new Token(TagType.None, "__") }); + yield return new TestCaseData("____", new[] { new Token(TagType.None, "____") }); + } + + public static IEnumerable CasesTextContainsHeaderTag() + { + yield return new TestCaseData("# wordA", new[] { new Token(TagType.Header, "wordA") }); + yield return new TestCaseData("# wordA # ", new[] { new Token(TagType.Header, "wordA # ") }); + yield return new TestCaseData(" # wordA", new[] { new Token(TagType.None, " # wordA") }); + + yield return new TestCaseData($"# wordA{Environment.NewLine} # ", + new[] + { + new Token(TagType.Header, "wordA"), + new Token(TagType.None, $"{Environment.NewLine} # ") + }); + + yield return new TestCaseData($" wordA{Environment.NewLine}{Environment.NewLine}# wordB", + new[] + { + new Token(TagType.None, $" wordA{Environment.NewLine}{Environment.NewLine}"), new Token(TagType.Header, "wordB") + }); + } + + public static IEnumerable CasesWhenTextContainsEscapingTag() + { + yield return new TestCaseData(@"\\", new[] { new Token(TagType.Escaping, @"\") }); + yield return new TestCaseData(@"\_wordA_", + new[] { new Token(TagType.Escaping, "_"), new Token(TagType.None, "wordA"), new Token(TagType.None, "_") }); + } + + public static IEnumerable CasesWhenTextContainsOverlappingTags() + { + yield return new TestCaseData("__wordA_wordB__wordC_", + new[] { new Token(TagType.None, "__wordA_wordB__wordC_") }); + yield return new TestCaseData("_wordA__wordB_wordC__ wordD", + new[] { new Token(TagType.None, "_wordA__wordB_wordC__ wordD") }); + } + + public static IEnumerable CasesWhenBoldTagInsideItalicTag() + { + yield return new TestCaseData("_wordA__wordB__wordC_", + new[] + { + new Token(TagType.Italic, "wordA__wordB__wordC", + [ + new Token(TagType.None, "wordA"), new Token(TagType.None, "__wordB__"), + new Token(TagType.None, "wordC") + ]) + }); + } + + public static IEnumerable CasesWhenItalicTagInsideBoldTag() + { + yield return new TestCaseData("__wordA_wordB_wordC__", + new[] + { + new Token(TagType.Bold, "wordA_wordB_wordC", + [ + new Token(TagType.None, "wordA"), new Token(TagType.Italic, "wordB"), + new Token(TagType.None, "wordC") + ]) + }); + } + + public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() + { + yield return new TestCaseData("wo__rd1__", + new[] { new Token(TagType.None, "wo"), new Token(TagType.None, "__rd1__") }); + yield return new TestCaseData("wo_rd1_", + new[] { new Token(TagType.None, "wo"), new Token(TagType.None, "_rd1_") }); + } + + public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() + { + yield return new TestCaseData("_wordA _", new[] { new Token(TagType.None, "_wordA _") }); + yield return new TestCaseData("__wordA __", new[] { new Token(TagType.None, "__wordA __") }); + yield return new TestCaseData("_ wordA_", + new[] { new Token(TagType.None, "_ wordA"), new Token(TagType.None, "_") }); + yield return new TestCaseData("__ wordA__", + new[] { new Token(TagType.None, "__ wordA"), new Token(TagType.None, "__") }); + } + + public static IEnumerable CasesWhenTextContainsBoldItalicTagsInMiddleWords() + { + yield return new TestCaseData("_wor_dA", + new[] { new Token(TagType.Italic, "wor"), new Token(TagType.None, "dA") }); + yield return new TestCaseData("__wor__dA", + new[] { new Token(TagType.Bold, "wor"), new Token(TagType.None, "dA") }); + yield return new TestCaseData("_wordA wor_dB", + new[] { new Token(TagType.None, "_wordA wor_"), new Token(TagType.None, "dB") }); + yield return new TestCaseData("__wordA wor__dB", + new[] { new Token(TagType.None, "__wordA wor__"), new Token(TagType.None, "dB") }); + } + + public static IEnumerable CasesWhenTextContainsLinkTag() + { + yield return new TestCaseData("[Name Link](https://www.example.com \"Tooltip\")", + new[] { new TokenTagLink("Name Link", "https://www.example.com", "Tooltip") }); + yield return new TestCaseData("[Name Link](https://www.example.com Tooltip\")", + new[] { new TokenTagLink("Name Link", "https://www.example.com Tooltip\"") }); + yield return new TestCaseData("Name Link](https://www.example.com Tooltip\")", + new[] { new Token(TagType.None, "Name Link](https://www.example.com Tooltip\")") }); + yield return new TestCaseData("[Name Link(https://www.example.com Tooltip\")", + new[] { new Token(TagType.None, "[Name Link(https://www.example.com Tooltip\")") }); + yield return new TestCaseData("[Name Link]https://www.example.com Tooltip\")", + new[] { new Token(TagType.None, "[Name Link]https://www.example.com Tooltip\")") }); + yield return new TestCaseData("[Name Link](https://www.example.com Tooltip\"", + new[] { new Token(TagType.None, "[Name Link](https://www.example.com Tooltip\"") }); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tests/MdTests.cs b/cs/Markdown/Tests/MdTests.cs new file mode 100644 index 000000000..a05aa5f4d --- /dev/null +++ b/cs/Markdown/Tests/MdTests.cs @@ -0,0 +1,237 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown.Tests; + +[TestFixture] +public class MdTests +{ + private Md markdown; + + [SetUp] + public void SetUp() + { + markdown = new Md(); + } + + [TestCase("wordA wordB")] + public void Render_ReturnsString_WhenTextWithoutTags(string input) + { + var result = markdown.Render(input); + + result.Should().Be("wordA wordB"); + } + + [TestCaseSource(nameof(CasesWhenTextWithOnePairedTag))] + [Description("Checks each paired tag")] + public void Render_ReturnsString_WhenTextWithOnePairedTag(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] + public void Render_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] + public void Render_ReturnsString_WhenTextWithMultipleNestedPairedTags(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithPairedTagWithoutPair))] + [Description("Checks each paired tag")] + public void Render_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] + public void Render_ReturnsString_WhenEmptyTextInsideTags(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesTextContainsHeaderTag))] + public void Render_ReturnsString_WhenTextContainsHeaderTag(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] + public void Render_ReturnsString_WhenTextContainsEscapingTag(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] + public void Render_ReturnsString_WhenTextContainsOverlappingTags(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] + public void Render_ReturnsString_WhenBoldTagInsideItalicTag(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] + public void Render_ReturnsString_WhenItalicTagInsideBoldTag(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] + public void Render_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] + public void Render_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] + public void Render_ReturnsString_WhenTextContainsBoldItalicTagsInMiddleWords(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsLinkTag))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsLinkTag(string input, string expectedResult) + { + var result = markdown.Render(input); + + result.Should().Be(expectedResult); + } + + public static IEnumerable CasesWhenTextWithOnePairedTag() + { + yield return new TestCaseData("_wordA wordB_", "wordA wordB"); + yield return new TestCaseData("__wordA wordB__", "wordA wordB"); + yield return new TestCaseData("wordA _wordB wordC_ wordD", "wordA wordB wordC wordD"); + yield return new TestCaseData("wordA __wordB wordC__ wordD", "wordA wordB wordC wordD"); + } + + public static IEnumerable CasesWhenTextWithMultipleNonNestedPairedTags() + { + yield return new TestCaseData("_wordA_ __wordB__ wordC", "wordA wordB wordC"); + } + + public static IEnumerable CasesWhenTextWithMultipleNestedPairedTags() + { + yield return new TestCaseData("__wordA _wordB_ wordC__", "wordA wordB wordC"); + } + + public static IEnumerable CasesWhenTextWithPairedTagWithoutPair() + { + yield return new TestCaseData("_wordA", "_wordA"); + yield return new TestCaseData("__wordA", "__wordA"); + } + + public static IEnumerable CasesWhenEmptyTextInsideTags() + { + yield return new TestCaseData("__", "__"); + yield return new TestCaseData("____", "____"); + } + + public static IEnumerable CasesTextContainsHeaderTag() + { + yield return new TestCaseData("# wordA", "

wordA

"); + yield return new TestCaseData("# wordA # ", "

wordA #

"); + yield return new TestCaseData(" # wordA", " # wordA"); + yield return new TestCaseData($"# wordA{Environment.NewLine} # ", $"

wordA

{Environment.NewLine} # "); + yield return new TestCaseData($" wordA{Environment.NewLine}{Environment.NewLine}# wordB", $" wordA{Environment.NewLine}{Environment.NewLine}

wordB

"); + } + + public static IEnumerable CasesWhenTextContainsEscapingTag() + { + yield return new TestCaseData(@"\\", @"\\"); + yield return new TestCaseData(@"\_wordA_", @"\_wordA_"); + } + + public static IEnumerable CasesWhenTextContainsOverlappingTags() + { + yield return new TestCaseData("__wordA_wordB__wordC_", "__wordA_wordB__wordC_"); + yield return new TestCaseData("_wordA__wordB_wordC__ wordD", "_wordA__wordB_wordC__ wordD"); + } + + public static IEnumerable CasesWhenBoldTagInsideItalicTag() + { + yield return new TestCaseData("_wordA__wordB__wordC_", "wordA__wordB__wordC"); + } + + public static IEnumerable CasesWhenItalicTagInsideBoldTag() + { + yield return new TestCaseData("__wordA_wordB_wordC__", "wordAwordBwordC"); + } + + public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() + { + yield return new TestCaseData("wo__rd1__", "wo__rd1__"); + yield return new TestCaseData("wo_rd1_", "wo_rd1_"); + } + + public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() + { + yield return new TestCaseData("_wordA _", "_wordA _"); + yield return new TestCaseData("__wordA __", "__wordA __"); + yield return new TestCaseData("_ wordA_", "_ wordA_"); + yield return new TestCaseData("__ wordA__", "__ wordA__"); + } + + public static IEnumerable CasesWhenTextContainsBoldItalicTagsInMiddleWords() + { + yield return new TestCaseData("_wor_dA", "wordA"); + yield return new TestCaseData("__wor__dA", "wordA"); + yield return new TestCaseData("_wordA wor_dB", "_wordA wor_dB"); + yield return new TestCaseData("__wordA wor__dB", "__wordA wor__dB"); + } + public static IEnumerable CasesWhenTextContainsLinkTag() + { + yield return new TestCaseData("[Name Link](https://www.example.com \"Tooltip\")", + "
Name Link"); + yield return new TestCaseData("[Name Link](https://www.example.com Tooltip\")", + "Name Link"); + yield return new TestCaseData("Name Link](https://www.example.com Tooltip\")", + "Name Link](https://www.example.com Tooltip\")"); + yield return new TestCaseData("[Name Link(https://www.example.com Tooltip\")", + "[Name Link(https://www.example.com Tooltip\")"); + yield return new TestCaseData("[Name Link]https://www.example.com Tooltip\")", + "[Name Link]https://www.example.com Tooltip\")"); + yield return new TestCaseData("[Name Link](https://www.example.com Tooltip\"", + "[Name Link](https://www.example.com Tooltip\""); + } + +} \ No newline at end of file diff --git a/cs/Markdown/Token/Token.cs b/cs/Markdown/Token/Token.cs new file mode 100644 index 000000000..2920fbb49 --- /dev/null +++ b/cs/Markdown/Token/Token.cs @@ -0,0 +1,10 @@ +namespace Markdown; + +public class Token(TagType tagType, string content, List? children = null) +{ + public TagType TagType { get; } = tagType; + + public string Content { get; } = content; + + public List? Children { get; } = children; +} \ No newline at end of file diff --git a/cs/Markdown/Token/TokenTagLink.cs b/cs/Markdown/Token/TokenTagLink.cs new file mode 100644 index 000000000..aef7c48a5 --- /dev/null +++ b/cs/Markdown/Token/TokenTagLink.cs @@ -0,0 +1,7 @@ +namespace Markdown; + +public class TokenTagLink(string content, string linkText, string? tooltipText = null, List? children = null) : Token(TagType.Link, content, children) +{ + public string? TooltipText { get; } = tooltipText; + public string LinkText { get; } = linkText; +} \ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..9e2352620 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{8794FD77-5E36-4990-A3CC-AB363934F2CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkMarkdown", "BenchmarkMarkdown\BenchmarkMarkdown.csproj", "{A85DA5D4-13A8-439F-A81A-CB37962627DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,5 +31,13 @@ Global {C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Release|Any CPU.Build.0 = Release|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Release|Any CPU.Build.0 = Release|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/cs/clean-code.sln.DotSettings b/cs/clean-code.sln.DotSettings index 135b83ecb..53fe49b2f 100644 --- a/cs/clean-code.sln.DotSettings +++ b/cs/clean-code.sln.DotSettings @@ -1,6 +1,9 @@  <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016