+ {
+ new EmHtmlToken("italic", 0),
+ new EmptyHtmlToken("empty", 0),
+ new StrongHtmlToken("bold", 0)
+ };
+
+ var token = new StrongHtmlToken(tokenList, 0);
+ token.Render(null).Should().Be("italicemptybold");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Test/MD_Should.cs b/Markdown/Test/MD_Should.cs
new file mode 100644
index 000000000..63f14d3ec
--- /dev/null
+++ b/Markdown/Test/MD_Should.cs
@@ -0,0 +1,211 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace Markdown.Test
+{
+ [TestFixture]
+ internal class Md_Should
+ {
+ [TestCase("qwe asd zxc", ExpectedResult = "qwe asd zxc")]
+ public string ParseNoMarkup(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase("_asd_", ExpectedResult = "asd")]
+ [TestCase("_a s d_", ExpectedResult = "a s d")]
+ [TestCase("_1_2_3_", ExpectedResult = "1_2_3")]
+ [TestCase("_a_ _s d_", ExpectedResult = "a s d")]
+ [TestCase("_a_ _s d", ExpectedResult = "a _s d")]
+ [TestCase("_aas__abc__abc_", ExpectedResult = "aas__abc__abc")]
+ public string ParseEmTagCorrectly(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase("_ d_", ExpectedResult = "_ d_")]
+ [TestCase("_a _", ExpectedResult = "_a _")]
+ [TestCase("__ d__", ExpectedResult = "__ d__")]
+ [TestCase("__a __", ExpectedResult = "__a __")]
+ [TestCase("_ abc_abc_", ExpectedResult = "_ abcabc")]
+ public string ParseTrailingWhitespaceCorrectly(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase("_ab cd", ExpectedResult = "_ab cd")]
+ [TestCase("__ab cd", ExpectedResult = "__ab cd")]
+ public string ParseNoMarkup_IfMissingCloseTag(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase(@"_a\_b_", ExpectedResult = "a_b")]
+ [TestCase(@"__a\_b__", ExpectedResult = "a_b")]
+ [TestCase(@"a\_b", ExpectedResult = "a_b")]
+ public string ParseEscapedCorrectly(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase("__abc_abc", ExpectedResult = "__abc_abc")]
+ [TestCase("__abc_abc_", ExpectedResult = "__abcabc")]
+ [TestCase("_abc__abc", ExpectedResult = "_abc__abc")]
+ [TestCase("_abc__abc__", ExpectedResult = "_abc__abc__")]
+ public string ParseUnpairTags(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase("__abc__", ExpectedResult = "abc")]
+ [TestCase("__abc_abc_abc__", ExpectedResult = "abcabcabc")]
+ public string ParseStrongTagCorrectrly(string plainMd)
+ {
+ return TrimPTag(new Md(plainMd).Render());
+ }
+
+ [TestCase("[url](www.url.com)", "", ExpectedResult = "url")]
+ [TestCase("[url](/url)", "www.base.com", ExpectedResult = "url")]
+ [TestCase("[url](www.url.com)\n[url](/url)", "www.base.com",
+ ExpectedResult = "url\nurl")]
+ public string ParseUrlTagCorrectrly(string plainMd, string baseUrl)
+ {
+ return TrimPTag(new Md(plainMd, baseUrl).Render());
+ }
+
+ [TestCase("_asd_", "css", "", ExpectedResult = "asd
", TestName = "No def")
+ ]
+ [TestCase("_asd_ __qwe__", "css", "",
+ ExpectedResult = "asd qwe
",
+ TestName = "No def, may tags")]
+ [TestCase("_asd_", "css", "definition\n",
+ ExpectedResult = "definition\nasd
",
+ TestName = "Defined")
+ ]
+ public string ParseWithDefinedCss(string plainMd, string cssClassName, string cssClassDef)
+ {
+ var css = new CssClassInfo(cssClassName, cssClassDef);
+
+ return new Md(plainMd, "", css).Render();
+ }
+
+ [TestCase("asd", ExpectedResult = "asd
")]
+ [TestCase("q\nb\nc", ExpectedResult = "q\nb\nc
")]
+ [TestCase("q\n\nb", ExpectedResult = "q
b
")]
+ public string ParseParagraphsCorrectly(string plainMd)
+ {
+ return new Md(plainMd).Render();
+ }
+
+ [TestCase("r _i_ r _i_", ExpectedResult = "r i r i
")]
+ [TestCase("r __b__ r __b__", ExpectedResult = "r b r b
")]
+ [TestCase("_i_ __b__ r", ExpectedResult = "i b r
")]
+ public string ParseMixedTagsCorrectrly(string plainMd)
+ {
+ return new Md(plainMd).Render();
+ }
+
+ [TestCase("##qwe", ExpectedResult = "qwe
", TestName = "Simple header")]
+ [TestCase("qwe\n##qwe\nqwe", ExpectedResult = "qwe
qwe
qwe
",
+ TestName = "Heaser with paragraphs")
+ ]
+ public string ParseHeaderCorrectly(string plainMd)
+ {
+ return new Md(plainMd).Render();
+ }
+
+ [TestCase(" This is a code block.", ExpectedResult = "This is a code block.
",
+ TestName = "One line code block")]
+ [TestCase("Here is an example of AppleScript:\n\ttell application \"Foo\"\n\t\tbeep\n\tend tell",
+ ExpectedResult =
+ "Here is an example of AppleScript:
tell application \"Foo\"\n\tbeep\nend tell
",
+ TestName = "Multiline code block")]
+ [TestCase("\t__thisIsNotTag__", ExpectedResult = "__thisIsNotTag__
",
+ TestName = "tags not render in code blocks")]
+ public string ParseCodeBlocksCorrectly(string plainMd)
+ {
+ return new Md(plainMd).Render();
+ }
+
+ [TestCase("1. Bird\n1. McHale\n1. Parish",
+ ExpectedResult = "- Bird
- McHale
- Parish
",
+ TestName = "List w/o paragraphs")]
+ [TestCase("1. Bird\n\n1. McHale\n1. Parish",
+ ExpectedResult = "Bird
- McHale
- Parish
",
+ TestName = "List with paragraphs")]
+ public string ParseOrderedListCorrectly(string plainMd)
+ {
+ return new Md(plainMd).Render();
+ }
+
+ private static string TrimPTag(string html)
+ {
+ if (html.StartsWith(""))
+ html = html.Substring(3);
+ if (html.EndsWith("
"))
+ html = html.Substring(0, html.Length - 4);
+ return html;
+ }
+
+ private static string GenerateMdTag(Tag tag, int length)
+ {
+ const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var rnd = new Random(100);
+ return tag.Equals(Tag.A)
+ ? $@"[{new string(
+ Enumerable
+ .Repeat(chars, length)
+ .Select(s => s[rnd.Next(s.Length)])
+ .ToArray())}]({new string(
+ Enumerable
+ .Repeat(chars, length)
+ .Select(s => s[rnd.Next(s.Length)])
+ .ToArray())})"
+ : $@"{tag.Md}{new string(
+ Enumerable
+ .Repeat(chars, length)
+ .Select(s => s[rnd.Next(s.Length)])
+ .ToArray())}{tag.Md}";
+ }
+
+ private static string GenerateMd(Random rnd)
+ {
+ var md = new StringBuilder();
+ for (var i = 0; i < 1000; i++)
+ md.Append(GenerateMdTag(Tag.GetRandomTag(rnd), 100000));
+ return md.ToString();
+ }
+
+ [Test]
+ [Explicit]
+ public void PerformanceTest()
+ {
+ var iterationWatch = new Stopwatch();
+ var parseWatch = new Stopwatch();
+ var rnd = new Random(10);
+ var plainMd = GenerateMd(rnd);
+ Console.WriteLine($"Length = {plainMd.Length}");
+
+ iterationWatch.Start();
+ for (var i = 0; i < plainMd.Length; i++)
+ // ReSharper disable once ReturnValueOfPureMethodIsNotUsed
+ rnd.Next();
+ iterationWatch.Stop();
+
+ var parser = new Md(plainMd);
+ parseWatch.Start();
+ parser.Render();
+ parseWatch.Stop();
+
+ Console.WriteLine(
+ $"iteration elapsed = {iterationWatch.ElapsedMilliseconds}, parse elapsed = {parseWatch.ElapsedMilliseconds}");
+ (parseWatch.ElapsedMilliseconds / iterationWatch.ElapsedMilliseconds)
+ .Should()
+ .BeLessThan(iterationWatch.ElapsedMilliseconds / 10);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/AHtmlToken.cs b/Markdown/Tokens/AHtmlToken.cs
new file mode 100644
index 000000000..ecb40b0dd
--- /dev/null
+++ b/Markdown/Tokens/AHtmlToken.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class AHtmlToken : HtmlToken
+ {
+ private readonly string url;
+ private readonly string baseUrl;
+ private bool IsReferece => url.StartsWith("/");
+ public override int Length => Data.Length + url.Length + 4 + EscapedCharacters;
+
+ public AHtmlToken(string data, string url, int escapedCharacters, string baseUrl) : base("a", data, escapedCharacters)
+ {
+ this.url = url;
+ this.baseUrl = baseUrl;
+ }
+
+ public AHtmlToken(List parsedTokens, int escapedCharacters)
+ : base("a", parsedTokens, escapedCharacters)
+ {
+ }
+
+ public override string Render(CssClassInfo cssClassInfo)
+ {
+ var buildedUrl = !IsReferece ? url : string.Join("", baseUrl, url);
+ return $"{Data}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/CodeHtmlToken.cs b/Markdown/Tokens/CodeHtmlToken.cs
new file mode 100644
index 000000000..924046cdd
--- /dev/null
+++ b/Markdown/Tokens/CodeHtmlToken.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class CodeHtmlToken : HtmlToken
+ {
+ public CodeHtmlToken(string data) : base("code", data, 0)
+ {
+ }
+
+ public CodeHtmlToken(List parsedTokens) : base("code", parsedTokens, 0)
+ {
+ }
+
+ public override string Render(CssClassInfo cssClassInfo)
+ {
+ return InsertInToTags("pre", InsertInToTags(Data, cssClassInfo), cssClassInfo);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/EmHtmlToken.cs b/Markdown/Tokens/EmHtmlToken.cs
new file mode 100644
index 000000000..8a5a40a1b
--- /dev/null
+++ b/Markdown/Tokens/EmHtmlToken.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class EmHtmlToken : HtmlToken
+ {
+ public override int Length => base.Length + 2;
+
+ public EmHtmlToken(string data, int escapedCharacters) : base("em", data, escapedCharacters)
+ {
+ }
+
+ public EmHtmlToken(List parsedTokens, int escapedCharacters)
+ : base("em", parsedTokens, escapedCharacters)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/EmptyHtmlToken.cs b/Markdown/Tokens/EmptyHtmlToken.cs
new file mode 100644
index 000000000..8f3655817
--- /dev/null
+++ b/Markdown/Tokens/EmptyHtmlToken.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Markdown.Tokens
+{
+ public class EmptyHtmlToken : HtmlToken
+ {
+ public EmptyHtmlToken(string data, int escapedCharacters) : base("", data, escapedCharacters)
+ {
+ }
+
+ public EmptyHtmlToken(List parsedTokens, int escapedCharacters)
+ : base("", parsedTokens, escapedCharacters)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/HHtmlToken.cs b/Markdown/Tokens/HHtmlToken.cs
new file mode 100644
index 000000000..9ee6bcf9e
--- /dev/null
+++ b/Markdown/Tokens/HHtmlToken.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class HHtmlToken : HtmlToken
+
+ {
+ private readonly int headerImportance;
+
+ public HHtmlToken(string data, int headerImportance) : base($"h{headerImportance}", data, 0)
+ {
+ this.headerImportance = headerImportance;
+ }
+
+ public HHtmlToken(List parsedTokens, int headerImportance) : base($"h{headerImportance}", parsedTokens, 0)
+ {
+ this.headerImportance = headerImportance;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/HtmlToken.cs b/Markdown/Tokens/HtmlToken.cs
new file mode 100644
index 000000000..cd15b314c
--- /dev/null
+++ b/Markdown/Tokens/HtmlToken.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Markdown.Tokens
+{
+ public abstract class HtmlToken
+ {
+ protected readonly string Tag;
+ protected readonly List ParsedTokens;
+ protected readonly string Data;
+
+ protected virtual bool IsTagged => !string.IsNullOrEmpty(Tag);
+
+ public virtual int Length => ParsedTokens.Sum(x => x.Length) + (Data ?? "").Length + EscapedCharacters;
+
+ protected readonly int EscapedCharacters;
+
+ protected HtmlToken(string tag, string data, int escapedCharacters)
+ {
+ Tag = tag;
+ Data = data;
+ EscapedCharacters = escapedCharacters;
+ ParsedTokens = new List();
+ }
+
+ protected HtmlToken(string tag, List parsedTokens, int escapedCharacters)
+ {
+ Tag = tag;
+ ParsedTokens = parsedTokens;
+ EscapedCharacters = escapedCharacters;
+ }
+
+ protected virtual string InsertInToTags(string dataToInsert, CssClassInfo cssClassInfo)
+ {
+ return IsTagged
+ ? InsertInToTags(Tag, dataToInsert, cssClassInfo)
+ : dataToInsert;
+ }
+
+ protected virtual string InsertInToTags(string tag, string dataToInsert, CssClassInfo cssClassInfo)
+ {
+ return $"<{tag}{GetCssClassDef(cssClassInfo)}>{dataToInsert}{tag}>";
+ }
+
+ protected static string GetCssClassDef(CssClassInfo cssClassInfo)
+ {
+ return cssClassInfo == null ? "" : $" class=\"{cssClassInfo.ClassName}\"";
+ }
+
+ public virtual string Render(CssClassInfo cssClassInfo)
+ {
+ return ParsedTokens.Count > 0
+ ? InsertInToTags(string.Join("", ParsedTokens.Select(token => token.Render(cssClassInfo))), cssClassInfo)
+ : InsertInToTags(Data, cssClassInfo);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/ListItemHtmlToken.cs b/Markdown/Tokens/ListItemHtmlToken.cs
new file mode 100644
index 000000000..34a898448
--- /dev/null
+++ b/Markdown/Tokens/ListItemHtmlToken.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Markdown.Tokens
+{
+ public class ListItemHtmlToken : HtmlToken
+ {
+ public ListItemHtmlToken(string data) : base("li", data, 0)
+ {
+ }
+
+ public ListItemHtmlToken(List parsedTokens) : base("li", parsedTokens, 0)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/OrderedListHtmlToken.cs b/Markdown/Tokens/OrderedListHtmlToken.cs
new file mode 100644
index 000000000..b9bbc7977
--- /dev/null
+++ b/Markdown/Tokens/OrderedListHtmlToken.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class OrderedListHtmlToken : HtmlToken
+ {
+ public OrderedListHtmlToken(string data) : base("ol", data, 0)
+ {
+ }
+
+ public OrderedListHtmlToken(List parsedTokens) : base("ol", parsedTokens, 0)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/PHtmlToken.cs b/Markdown/Tokens/PHtmlToken.cs
new file mode 100644
index 000000000..c2c61ac93
--- /dev/null
+++ b/Markdown/Tokens/PHtmlToken.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class PHtmlToken : HtmlToken
+ {
+ public PHtmlToken(string data) : base("p", data, 0)
+ {
+ }
+
+ public PHtmlToken(List parsedTokens) : base("p", parsedTokens, 0)
+ {
+ }
+
+ public ListItemHtmlToken ToListItem()
+ {
+ return new ListItemHtmlToken(ParsedTokens);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/Tokens/StrongHtmlToken.cs b/Markdown/Tokens/StrongHtmlToken.cs
new file mode 100644
index 000000000..8ed4ebbcc
--- /dev/null
+++ b/Markdown/Tokens/StrongHtmlToken.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Markdown.Tokens
+{
+ public class StrongHtmlToken : HtmlToken
+ {
+ public override int Length => base.Length + 4;
+
+ public StrongHtmlToken(string data, int escapedCharacters) : base("strong", data, escapedCharacters)
+ {
+ }
+
+ public StrongHtmlToken(List parsedTokens, int escapedCharacters)
+ : base("strong", parsedTokens, escapedCharacters)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Markdown/packages.config b/Markdown/packages.config
new file mode 100644
index 000000000..939c1055b
--- /dev/null
+++ b/Markdown/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/clean-code.sln b/clean-code.sln
index 25a6075ae..80dc2198e 100644
--- a/clean-code.sln
+++ b/clean-code.sln
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{9ED89DE1-D257-450E-B66C-3B28D10F331D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{E6825F19-EE20-40A7-AE68-22051E2AF040}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +55,18 @@ Global
{9ED89DE1-D257-450E-B66C-3B28D10F331D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{9ED89DE1-D257-450E-B66C-3B28D10F331D}.Release|x86.ActiveCfg = Release|Any CPU
{9ED89DE1-D257-450E-B66C-3B28D10F331D}.Release|x86.Build.0 = Release|Any CPU
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Debug|Mixed Platforms.ActiveCfg = Debug|Mixed Platforms
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Debug|Mixed Platforms.Build.0 = Debug|Mixed Platforms
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Debug|x86.ActiveCfg = Debug|x86
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Debug|x86.Build.0 = Debug|x86
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Release|Mixed Platforms.ActiveCfg = Release|Mixed Platforms
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Release|Mixed Platforms.Build.0 = Release|Mixed Platforms
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Release|x86.ActiveCfg = Release|x86
+ {E6825F19-EE20-40A7-AE68-22051E2AF040}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE