diff --git a/Chess/ChessProblem_Test.cs b/Chess/ChessProblem_Test.cs index dd559b3d7..fc3c9beee 100644 --- a/Chess/ChessProblem_Test.cs +++ b/Chess/ChessProblem_Test.cs @@ -1,55 +1,55 @@ -using System; -using System.IO; -using NUnit.Framework; - -namespace Chess -{ - [TestFixture] - public class ChessProblem_Test - { - private static void TestOnFile(string filename) - { - var board = File.ReadAllLines(filename); - ChessProblem.LoadFrom(board); - var expectedAnswer = File.ReadAllText(Path.ChangeExtension(filename, ".ans")).Trim(); - ChessProblem.CalculateChessStatus(); - Assert.AreEqual(expectedAnswer, ChessProblem.ChessStatus.ToString().ToLower(), "Failed test " + filename); - } - - [Test] - public void RepeatedMethodCallDoNotChangeBehaviour() - { - var board = new[] - { - " ", - " ", - " ", - " q ", - " K ", - " Q ", - " ", - " ", - }; - ChessProblem.LoadFrom(board); - ChessProblem.CalculateChessStatus(); - Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); - - // Now check that internal board modifictions during the first call do not change answer - ChessProblem.CalculateChessStatus(); - Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); - } - - [Test] - public void FullTests() - { - var dir = TestContext.CurrentContext.TestDirectory; - var testsCount = 0; - foreach (var filename in Directory.GetFiles(Path.Combine(dir, "ChessTests"), "*.in")) - { - TestOnFile(filename); - testsCount++; - } - Console.WriteLine("Tests passed: " + testsCount); - } - } -} \ No newline at end of file +//using System; +//using System.IO; +//using NUnit.Framework; +// +//namespace Chess +//{ +// [TestFixture] +// public class ChessProblem_Test +// { +// private static void TestOnFile(string filename) +// { +// var board = File.ReadAllLines(filename); +// ChessProblem.LoadFrom(board); +// var expectedAnswer = File.ReadAllText(Path.ChangeExtension(filename, ".ans")).Trim(); +// ChessProblem.CalculateChessStatus(); +// Assert.AreEqual(expectedAnswer, ChessProblem.ChessStatus.ToString().ToLower(), "Failed test " + filename); +// } +// +// [Test] +// public void RepeatedMethodCallDoNotChangeBehaviour() +// { +// var board = new[] +// { +// " ", +// " ", +// " ", +// " q ", +// " K ", +// " Q ", +// " ", +// " ", +// }; +// ChessProblem.LoadFrom(board); +// ChessProblem.CalculateChessStatus(); +// Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); +// +// // Now check that internal board modifictions during the first call do not change answer +// ChessProblem.CalculateChessStatus(); +// Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); +// } +// +// [Test] +// public void FullTests() +// { +// var dir = TestContext.CurrentContext.TestDirectory; +// var testsCount = 0; +// foreach (var filename in Directory.GetFiles(Path.Combine(dir, "ChessTests"), "*.in")) +// { +// TestOnFile(filename); +// testsCount++; +// } +// Console.WriteLine("Tests passed: " + testsCount); +// } +// } +//} \ No newline at end of file diff --git a/ControlDigit/ControlDigitExtensions.cs b/ControlDigit/ControlDigitExtensions.cs index 807fc087c..d3291bdac 100644 --- a/ControlDigit/ControlDigitExtensions.cs +++ b/ControlDigit/ControlDigitExtensions.cs @@ -32,45 +32,45 @@ public static int ControlDigit2(this long number) } } - [TestFixture] - public class ControlDigitExtensions_Tests - { - [TestCase(0, ExpectedResult = 0)] - [TestCase(1, ExpectedResult = 1)] - [TestCase(2, ExpectedResult = 2)] - [TestCase(9, ExpectedResult = 9)] - [TestCase(10, ExpectedResult = 3)] - [TestCase(15, ExpectedResult = 8)] - [TestCase(17, ExpectedResult = 1)] - [TestCase(18, ExpectedResult = 0)] - public int TestControlDigit(long x) - { - return x.ControlDigit(); - } - - [Test] - public void CompareImplementations() - { - for (long i = 0; i < 100000; i++) - Assert.AreEqual(i.ControlDigit(), i.ControlDigit2()); - } - } - - [TestFixture] - public class ControlDigit_PerformanceTests - { - [Test] - public void TestControlDigitSpeed() - { - var count = 10000000; - var sw = Stopwatch.StartNew(); - for (int i = 0; i < count; i++) - 12345678L.ControlDigit(); - Console.WriteLine("Old " + sw.Elapsed); - sw.Restart(); - for (int i = 0; i < count; i++) - 12345678L.ControlDigit2(); - Console.WriteLine("New " + sw.Elapsed); - } - } +// [TestFixture] +// public class ControlDigitExtensions_Tests +// { +// [TestCase(0, ExpectedResult = 0)] +// [TestCase(1, ExpectedResult = 1)] +// [TestCase(2, ExpectedResult = 2)] +// [TestCase(9, ExpectedResult = 9)] +// [TestCase(10, ExpectedResult = 3)] +// [TestCase(15, ExpectedResult = 8)] +// [TestCase(17, ExpectedResult = 1)] +// [TestCase(18, ExpectedResult = 0)] +// public int TestControlDigit(long x) +// { +// return x.ControlDigit(); +// } +// +// [Test] +// public void CompareImplementations() +// { +// for (long i = 0; i < 100000; i++) +// Assert.AreEqual(i.ControlDigit(), i.ControlDigit2()); +// } +// } +// +// [TestFixture] +// public class ControlDigit_PerformanceTests +// { +// [Test] +// public void TestControlDigitSpeed() +// { +// var count = 10000000; +// var sw = Stopwatch.StartNew(); +// for (int i = 0; i < count; i++) +// 12345678L.ControlDigit(); +// Console.WriteLine("Old " + sw.Elapsed); +// sw.Restart(); +// for (int i = 0; i < count; i++) +// 12345678L.ControlDigit2(); +// Console.WriteLine("New " + sw.Elapsed); +// } +// } } diff --git a/Markdown/CssClassInfo.cs b/Markdown/CssClassInfo.cs new file mode 100644 index 000000000..368704822 --- /dev/null +++ b/Markdown/CssClassInfo.cs @@ -0,0 +1,14 @@ +namespace Markdown +{ + public class CssClassInfo + { + public readonly string ClassName; + public readonly string Description; + + public CssClassInfo(string className, string description) + { + Description = description; + ClassName = className; + } + } +} \ No newline at end of file diff --git a/Markdown/HtmlToken.cs b/Markdown/HtmlToken.cs deleted file mode 100644 index 9cfe3fc97..000000000 --- a/Markdown/HtmlToken.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Markdown -{ - public class HtmlToken - { - private readonly Tag tag; - private readonly List parsedTokens; - private readonly string data; - private bool IsTagged => !tag.Equals(Tag.Empty); - public int Length => parsedTokens.Sum(x => x.Length) + (data ?? "").Length + escapedCharacters + tag.Md.Length * 2; - private readonly int escapedCharacters; - - public HtmlToken(Tag tag, string data, int escapedCharacters) - { - this.tag = tag; - this.data = data; - this.escapedCharacters = escapedCharacters; - parsedTokens = new List(); - } - - public HtmlToken(Tag tag, List parsedTokens, int escapedCharacters) - { - this.tag = tag; - this.parsedTokens = parsedTokens; - this.escapedCharacters = escapedCharacters; - } - - private string InsertInToTags(string dataToInsert) => IsTagged - ? $"<{tag.Html}>{dataToInsert}" - : dataToInsert; - - public override string ToString() - { - return parsedTokens.Count > 0 - ? InsertInToTags(string.Join("", parsedTokens.Select(token => token.ToString()))) - : InsertInToTags(data); - } - } -} \ No newline at end of file diff --git a/Markdown/LineType.cs b/Markdown/LineType.cs new file mode 100644 index 000000000..57e5235ce --- /dev/null +++ b/Markdown/LineType.cs @@ -0,0 +1,10 @@ +namespace Markdown +{ + public enum LineType + { + Header, + CodeBlock, + OrderedList, + Simple + } +} \ No newline at end of file diff --git a/Markdown/Markdown.csproj b/Markdown/Markdown.csproj index bfccc7366..17a2d2fab 100644 --- a/Markdown/Markdown.csproj +++ b/Markdown/Markdown.csproj @@ -10,8 +10,9 @@ Properties Markdown Markdown - v4.5 + v4.6 512 + 6 AnyCPU @@ -39,6 +40,9 @@ ..\packages\FluentAssertions.4.16.0\lib\net45\FluentAssertions.Core.dll + + ..\..\..\..\..\Library\Frameworks\Mono.framework\Versions\4.8.0\lib\mono\4.5-api\Microsoft.CSharp.dll + ..\packages\NUnit.3.5.0\lib\net45\nunit.framework.dll @@ -50,10 +54,22 @@ + + - + + + + + + + + + + + diff --git a/Markdown/Md.cs b/Markdown/Md.cs index 229d5dda3..e0e4a8727 100644 --- a/Markdown/Md.cs +++ b/Markdown/Md.cs @@ -2,44 +2,153 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Markdown.Tokens; namespace Markdown { public class Md { - private readonly string plainMd; + private readonly string[] plainMd; + private readonly string baseUrl; + private readonly CssClassInfo cssClassInfo; - private readonly Dictionary> mdTagParserFuncMatch; - private readonly Dictionary> validateFunctions; + private readonly Dictionary> stringTagParserFuncMatch; - public Md(string plainMd) + private static readonly Dictionary> ValidateFunctions = new Dictionary + > + { + [Tag.Em] = IsValidEmTag, + [Tag.Strong] = IsValidStrongTag, + [Tag.Empty] = (q, w, e) => false, + [Tag.A] = IsValidATag + }; + + private readonly Dictionary> lineTagParserFuncMatch; + + private int currLineIndex; + + private string CurrLine { - this.plainMd = plainMd; - mdTagParserFuncMatch = new Dictionary> + get { return plainMd[currLineIndex]; } + set { plainMd[currLineIndex] = value; } + } + + private bool IsInPlainMd => currLineIndex < plainMd.Length; + + public Md(string plainMd, string baseUrl = "", CssClassInfo cssClassInfo = null) + { + this.plainMd = plainMd.Split('\n'); + this.baseUrl = baseUrl; + this.cssClassInfo = cssClassInfo; + + stringTagParserFuncMatch = new Dictionary> { - [Tag.Em] = ParseEmToken, + [Tag.Em] = ParseItalic, [Tag.Empty] = ParseNoMarkup, - [Tag.Strong] = ParseStrongToken + [Tag.Strong] = ParseBold, + [Tag.A] = ParseUrl, }; - validateFunctions = new Dictionary> + + lineTagParserFuncMatch = new Dictionary> { - [Tag.Em] = IsValidEmTag, - [Tag.Strong] = IsValidStrongTag, - [Tag.Empty] = (i, b) => false + [LineType.Header] = ParseHeader, + [LineType.Simple] = ParseParagraph, + [LineType.CodeBlock] = ParseCodeBlock, + [LineType.OrderedList] = ParseOrderedList }; + + currLineIndex = 0; + } + + private HtmlToken ParseHeader() + { + var headerText = CurrLine.Replace("#", "").Replace("\\", ""); + + var headerImportance = CurrLine.Length - headerText.Length; + + currLineIndex++; + return new HHtmlToken(headerText, headerImportance); + } + + private HtmlToken ParseCodeBlock() + { + var builder = new StringBuilder(); + + while (IsInPlainMd && GetLineTypeTag(CurrLine) == LineType.CodeBlock) + { + builder.Append(CurrLine.Substring(CurrLine.StartsWith("\t") ? 1 : 4)); + builder.Append("\n"); + currLineIndex++; + } + + builder.Remove(builder.Length - 1, 1); + + return new CodeHtmlToken(builder.ToString()); } - private HtmlToken ParseEmToken(int index, string alreadyParsed = "", int alreadyEscaped = 0) + private HtmlToken ParseOrderedList() { - if (!IsValidEmTag(index, true)) - return ParseNoMarkup(index); + var listItemsTokens = new List(); + + while (IsInPlainMd && GetLineTypeTag(CurrLine) == LineType.OrderedList) + listItemsTokens.Add(ParseListItem()); + + return new OrderedListHtmlToken(listItemsTokens); + } + + private HtmlToken ParseListItem() + { + CurrLine = CurrLine.Substring(CurrLine.IndexOf(' ') + 1); + + var parsedToken = ParseParagraph(); + + var isParagraph = false; + if (currLineIndex != plainMd.Length && string.IsNullOrWhiteSpace(CurrLine)) + { + isParagraph = true; + currLineIndex++; + } + + return isParagraph + ? new ListItemHtmlToken(new List {parsedToken}) + : ((PHtmlToken) parsedToken).ToListItem(); + } + + private HtmlToken ParseParagraph() + { + var innerTags = new List(); + + while (IsInPlainMd && !string.IsNullOrWhiteSpace(CurrLine) && GetLineTypeTag(CurrLine) == LineType.Simple) + { + if (innerTags.Count != 0) + innerTags.Add(new EmptyHtmlToken("\n", 0)); + + var i = 0; + while (i < CurrLine.Length) + { + var tag = ParseTag(CurrLine, i); + var parsedToken = stringTagParserFuncMatch[tag].Invoke(CurrLine, i, "", 0); + i += parsedToken.Length; + innerTags.Add(parsedToken); + } + + currLineIndex++; + } + + return new PHtmlToken(innerTags); + } + + private static HtmlToken ParseItalic(string currLine, int index, string alreadyParsed = "", int alreadyEscaped = 0) + { + if (!IsValidEmTag(currLine, index, true)) + return ParseNoMarkup(currLine, index); index++; var tokenData = new StringBuilder(alreadyParsed); - while (index < plainMd.Length && !IsValidEmTag(index, false)) + while (index < currLine.Length && !IsValidEmTag(currLine, index, false)) { - var tag = ParseTag(index); + var tag = ParseTag(currLine, index); if (tag.Equals(Tag.Strong)) { @@ -48,156 +157,215 @@ private HtmlToken ParseEmToken(int index, string alreadyParsed = "", int already continue; } - if (plainMd[index] == '\\') + if (currLine[index] == '\\') { index++; alreadyEscaped++; } - tokenData.Append(plainMd[index]); + tokenData.Append(currLine[index]); index++; } - return index != plainMd.Length - ? new HtmlToken(Tag.Em, tokenData.ToString(), alreadyEscaped) - : new HtmlToken(Tag.Empty, tokenData.Insert(0, '_').ToString(), alreadyEscaped); + return index != currLine.Length + ? (HtmlToken) new EmHtmlToken(tokenData.ToString(), alreadyEscaped) + : new EmptyHtmlToken(tokenData.Insert(0, '_').ToString(), alreadyEscaped); } - private HtmlToken ParseStrongToken(int index, string alreadyParsed = "", int alreadyEscaped = 0) + private static HtmlToken ParseBold(string currLine, int index, string alreadyParsed = "", + int alreadyEscaped = 0) { - if (!IsValidStrongTag(index, true)) - return ParseNoMarkup(index); + if (!IsValidStrongTag(currLine, index, true)) + return ParseNoMarkup(currLine, index); var parsedTokens = new List(); index += 2; var tokenData = new StringBuilder(alreadyParsed); - while (index < plainMd.Length && !IsValidStrongTag(index, false)) + while (index < currLine.Length && !IsValidStrongTag(currLine, index, false)) { - var tag = ParseTag(index); + var tag = ParseTag(currLine, index); if (Equals(tag, Tag.Em)) { - parsedTokens.Add(ParseEmInStrong(ref index, ref alreadyEscaped, parsedTokens, tokenData)); - if (index == plainMd.Length) + parsedTokens.Add(ParseEmInStrong(currLine, ref index, ref alreadyEscaped, parsedTokens, tokenData)); + if (index == currLine.Length) break; } - if (plainMd[index] == '\\') + if (currLine[index] == '\\') { index++; alreadyEscaped++; } - tokenData.Append(plainMd[index]); + tokenData.Append(currLine[index]); index++; } - parsedTokens.Add(new HtmlToken(Tag.Empty, tokenData.ToString(), alreadyEscaped)); - return index != plainMd.Length - ? new HtmlToken(Tag.Strong, parsedTokens, 0) - : new HtmlToken(Tag.Empty, tokenData.Insert(0, "__").ToString(), alreadyEscaped); + parsedTokens.Add(new EmptyHtmlToken(tokenData.ToString(), alreadyEscaped)); + return index != currLine.Length + ? (HtmlToken) new StrongHtmlToken(parsedTokens, 0) + : new EmptyHtmlToken(tokenData.Insert(0, "__").ToString(), alreadyEscaped); } - private HtmlToken ParseEmInStrong(ref int index, ref int alreadyEscaped, - ICollection parsedTokens, StringBuilder tokenData) - { - parsedTokens.Add(new HtmlToken(Tag.Empty, tokenData.ToString(), alreadyEscaped)); - alreadyEscaped = 0; - tokenData.Clear(); - var htmlToken = ParseEmToken(index); - index += htmlToken.Length; - return htmlToken; - } - - private HtmlToken ParseNoMarkup(int index, string alreadyParsed = "", int alreadyEscaped = 0) + private static HtmlToken ParseNoMarkup(string currLine, int index, string alreadyParsed = "", int alreadyEscaped = 0) { var tokenData = new StringBuilder(alreadyParsed); var escaped = alreadyEscaped; - while (index < plainMd.Length) + while (index < currLine.Length) { - var tag = ParseTag(index); + var tag = ParseTag(currLine, index); - if (validateFunctions[tag].Invoke(index, true)) + if (ValidateFunctions[tag].Invoke(currLine, index, true)) break; - if (plainMd[index] == '\\') + if (currLine[index] == '\\') { index++; escaped++; } - tokenData.Append(plainMd[index]); + tokenData.Append(currLine[index]); index++; } - return new HtmlToken(Tag.Empty, tokenData.ToString(), escaped); + return new EmptyHtmlToken(tokenData.ToString(), escaped); } - private bool NotInsideDigits(int tagIndex) + private HtmlToken ParseUrl(string currLine, int index, string alreadyParsed = "", int alreadyEscaped = 0) { - if (tagIndex + 1 == plainMd.Length || tagIndex - 1 == -1) + var returnedValue = ParseInsideBracers(']', index, alreadyEscaped, alreadyParsed, currLine); + var escaped = returnedValue.Item2; + index = returnedValue.Item1; + var urlText = returnedValue.Item3; + + if (currLine[index] != '(') + throw new MdParserException($"Can't parse link at index {index}"); + returnedValue = ParseInsideBracers(')', index, alreadyEscaped, alreadyParsed, currLine); + return new AHtmlToken(urlText, returnedValue.Item3, escaped + returnedValue.Item2, baseUrl); + } + + private static Tuple ParseInsideBracers(char closeBracer, int index, int escaped, + string alreadyParsed, string currLine) + { + var data = new StringBuilder(alreadyParsed); + index++; + while (index < currLine.Length && currLine[index] != closeBracer) + { + if (currLine[index] == '\\') + { + index++; + escaped++; + } + data.Append(currLine[index]); + index++; + } + index++; + var dataStr = data.ToString(); + return Tuple.Create(index, escaped, dataStr); + } + + private static HtmlToken ParseEmInStrong(string currLine, ref int index, ref int alreadyEscaped, + ICollection parsedTokens, StringBuilder tokenData) + { + parsedTokens.Add(new EmptyHtmlToken(tokenData.ToString(), alreadyEscaped)); + alreadyEscaped = 0; + tokenData.Clear(); + var htmlToken = ParseItalic(currLine, index); + index += htmlToken.Length; + return htmlToken; + } + + private static bool NotInsideDigits(int tagIndex, string currLine) + { + if (tagIndex + 1 == currLine.Length || tagIndex - 1 == -1) return true; - return !char.IsDigit(plainMd[tagIndex - 1]) && !char.IsDigit(plainMd[tagIndex + 1]); + return !char.IsDigit(currLine[tagIndex - 1]) && !char.IsDigit(currLine[tagIndex + 1]); } - private bool IsNotStrongTag(int tagIndex) - => !(tagIndex - 1 != -1 && plainMd[tagIndex - 1] == '_' || - tagIndex + 1 != plainMd.Length && plainMd[tagIndex + 1] == '_'); + private static bool IsNotStrongTag(int tagIndex, string currLine) + { + return !(tagIndex - 1 != -1 && currLine[tagIndex - 1] == '_' || + tagIndex + 1 != currLine.Length && currLine[tagIndex + 1] == '_'); + } - private bool NoSpaceNearMdTag(int tagIndex, int tagLength, bool isOpenTag) + private static bool NoSpaceNearMdTag(int tagIndex, int tagLength, bool isOpenTag, string currLine) { var nextIndex = tagIndex + (isOpenTag ? tagLength : -1); - return nextIndex >= 0 && nextIndex < plainMd.Length && plainMd[nextIndex] != ' '; + return nextIndex >= 0 && nextIndex < currLine.Length && currLine[nextIndex] != ' '; } - private bool IsNotOpenTagInEndOfString(int tagIndex, int tagLength, bool isOpenTag) - => !(tagIndex == plainMd.Length - tagLength && isOpenTag); + private static bool IsNotOpenTagInEndOfString(int tagIndex, int tagLength, bool isOpenTag, string currLine) + { + return !(tagIndex == currLine.Length - tagLength && isOpenTag); + } - private bool IsValidEmTag(int tagIndex, bool isOpenTag) + private static bool IsValidEmTag(string currLine, int tagIndex, bool isOpenTag) { - if (plainMd[tagIndex] != '_') + if (currLine[tagIndex] != '_') return false; - return IsNotOpenTagInEndOfString(tagIndex, 1, isOpenTag) - && NoSpaceNearMdTag(tagIndex, 1, isOpenTag) - && IsNotStrongTag(tagIndex) - && NotInsideDigits(tagIndex); + return IsNotOpenTagInEndOfString(tagIndex, 1, isOpenTag, currLine) + && NoSpaceNearMdTag(tagIndex, 1, isOpenTag, currLine) + && IsNotStrongTag(tagIndex, currLine) + && NotInsideDigits(tagIndex, currLine); } - private bool IsValidStrongTag(int tagIndex, bool isOpenTag) + private static bool IsValidStrongTag(string currLine, int tagIndex, bool isOpenTag) { - if (plainMd[tagIndex] != '_' || !ParseTag(tagIndex).Equals(Tag.Strong)) + if (currLine[tagIndex] != '_' || !ParseTag(currLine, tagIndex).Equals(Tag.Strong)) return false; - return IsNotOpenTagInEndOfString(tagIndex, 2, isOpenTag) - && NoSpaceNearMdTag(tagIndex, 2, isOpenTag); + return IsNotOpenTagInEndOfString(tagIndex, 2, isOpenTag, currLine) + && NoSpaceNearMdTag(tagIndex, 2, isOpenTag, currLine); } - private Tag ParseTag(int tagIndex) + private static bool IsValidATag(string currLine, int tagIndex, bool isOpenTag) { - if (plainMd[tagIndex] == '_') - { - if (tagIndex != plainMd.Length - 1) - return plainMd[tagIndex + 1] == '_' ? Tag.Strong : Tag.Em; + return currLine[tagIndex] == '['; + } + + private static Tag ParseTag(string currLine, int tagIndex) + { + if (currLine[tagIndex] == '[') + return Tag.A; + if (currLine[tagIndex] != '_') + return Tag.Empty; + if (tagIndex == currLine.Length - 1) return Tag.Em; - } - return Tag.Empty; + return currLine[tagIndex + 1] == '_' + ? Tag.Strong + : Tag.Em; + } + + private static LineType GetLineTypeTag(string currLine) + { + if (currLine.StartsWith("#")) + return LineType.Header; + if (currLine.StartsWith(" ") || currLine.StartsWith("\t")) + return LineType.CodeBlock; + if (char.IsDigit(currLine[0])) + return LineType.OrderedList; + return LineType.Simple; } private IEnumerable TryParseToHtml() { - var i = 0; var root = new List(); - while (i < plainMd.Length) + + while (currLineIndex < plainMd.Length) { - var tag = ParseTag(i); - var parsedToken = mdTagParserFuncMatch[tag].Invoke(i, "", 0); - i += parsedToken.Length; - root.Add(parsedToken); + var htmlToken = lineTagParserFuncMatch[GetLineTypeTag(CurrLine)].Invoke(); + root.Add(htmlToken); + while (currLineIndex < plainMd.Length && string.IsNullOrWhiteSpace(CurrLine) && string.IsNullOrEmpty(CurrLine)) + currLineIndex++; } + return root; } public string Render() { var htmlTokens = TryParseToHtml(); - return string.Join("", htmlTokens.Select(x => x.ToString())); + return string.Join("", + cssClassInfo == null ? "" : cssClassInfo.Description, + string.Join("", htmlTokens.Select(x => x.Render(cssClassInfo)))); } } } \ No newline at end of file diff --git a/Markdown/MdParserException.cs b/Markdown/MdParserException.cs new file mode 100644 index 000000000..792c18634 --- /dev/null +++ b/Markdown/MdParserException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Markdown +{ + public class MdParserException : Exception + { + public MdParserException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/Markdown/Tag.cs b/Markdown/Tag.cs index 13e6a44a0..84d95effa 100644 --- a/Markdown/Tag.cs +++ b/Markdown/Tag.cs @@ -10,6 +10,8 @@ public class Tag public static readonly Tag Em = new Tag("_", "em"); public static readonly Tag Strong = new Tag("__", "strong"); public static readonly Tag Empty = new Tag("", ""); + public static readonly Tag A = new Tag("", "a"); + public static readonly Tag H = new Tag("#", "h"); private static readonly List Tags = new List {Em, Strong, Empty}; private Tag(string md, string html) @@ -18,7 +20,10 @@ private Tag(string md, string html) Html = html; } - public static Tag GetRandomTag(Random rnd) => Tags[rnd.Next(Tags.Count)]; + public static Tag GetRandomTag(Random rnd) + { + return Tags[rnd.Next(Tags.Count)]; + } public override bool Equals(object other) { diff --git a/Markdown/Test/HtmlToken_Should.cs b/Markdown/Test/HtmlToken_Should.cs index 4eb05377b..de979fbd4 100644 --- a/Markdown/Test/HtmlToken_Should.cs +++ b/Markdown/Test/HtmlToken_Should.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using NUnit.Framework; using FluentAssertions; +using Markdown.Tokens; + namespace Markdown.Test { [TestFixture] @@ -9,9 +11,9 @@ public class HtmlToken_Should [Test] public void ShouldInsertDataInToTags_WhenToStringCalls() { - var token = new HtmlToken(Tag.Em, "data", 0); + var token = new EmHtmlToken("data", 0); - token.ToString().Should().Be("data"); + token.Render(null).Should().Be("data"); } [Test] @@ -19,13 +21,13 @@ public void ShouldConcatManyTags_WhenHasInsertedTags() { var tokenList = new List { - new HtmlToken(Tag.Em, "italic", 0), - new HtmlToken(Tag.Empty, "empty", 0), - new HtmlToken(Tag.Strong, "bold", 0) + new EmHtmlToken("italic", 0), + new EmptyHtmlToken("empty", 0), + new StrongHtmlToken("bold", 0) }; - var token = new HtmlToken(Tag.Strong, tokenList, 0); - token.ToString().Should().Be("italicemptybold"); + 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 index 71458e48f..63f14d3ec 100644 --- a/Markdown/Test/MD_Should.cs +++ b/Markdown/Test/MD_Should.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Linq.Expressions; using System.Text; using FluentAssertions; using NUnit.Framework; @@ -15,7 +13,7 @@ internal class Md_Should [TestCase("qwe asd zxc", ExpectedResult = "qwe asd zxc")] public string ParseNoMarkup(string plainMd) { - return new Md(plainMd).Render(); + return TrimPTag(new Md(plainMd).Render()); } [TestCase("_asd_", ExpectedResult = "asd")] @@ -26,7 +24,7 @@ public string ParseNoMarkup(string plainMd) [TestCase("_aas__abc__abc_", ExpectedResult = "aas__abc__abc")] public string ParseEmTagCorrectly(string plainMd) { - return new Md(plainMd).Render(); + return TrimPTag(new Md(plainMd).Render()); } [TestCase("_ d_", ExpectedResult = "_ d_")] @@ -36,14 +34,14 @@ public string ParseEmTagCorrectly(string plainMd) [TestCase("_ abc_abc_", ExpectedResult = "_ abcabc")] public string ParseTrailingWhitespaceCorrectly(string plainMd) { - return new Md(plainMd).Render(); + 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 new Md(plainMd).Render(); + return TrimPTag(new Md(plainMd).Render()); } [TestCase(@"_a\_b_", ExpectedResult = "a_b")] @@ -51,7 +49,7 @@ public string ParseNoMarkup_IfMissingCloseTag(string plainMd) [TestCase(@"a\_b", ExpectedResult = "a_b")] public string ParseEscapedCorrectly(string plainMd) { - return new Md(plainMd).Render(); + return TrimPTag(new Md(plainMd).Render()); } [TestCase("__abc_abc", ExpectedResult = "__abc_abc")] @@ -60,56 +58,147 @@ public string ParseEscapedCorrectly(string plainMd) [TestCase("_abc__abc__", ExpectedResult = "_abc__abc__")] public string ParseUnpairTags(string plainMd) { - return new Md(plainMd).Render(); + 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\n

asd

", + 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")] + [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(); } - private static string GenerateMd(Tag tag, int length) + [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 = "
  1. Bird
  2. McHale
  3. Parish
", + TestName = "List w/o paragraphs")] + [TestCase("1. Bird\n\n1. McHale\n1. Parish", + ExpectedResult = "
  1. Bird

  2. McHale
  3. 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.Md}{new string( - Enumerable - .Repeat(chars, length) - .Select(s => s[rnd.Next(s.Length)]) - .ToArray())}{tag.Md}"; + 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 md = new StringBuilder(); var rnd = new Random(10); - for (var i = 0; i < 1000; i++) - md.Append(GenerateMd(Tag.GetRandomTag(rnd), 100000)); - var plainMd = md.ToString(); + var plainMd = GenerateMd(rnd); Console.WriteLine($"Length = {plainMd.Length}"); - var c = 0; iterationWatch.Start(); for (var i = 0; i < plainMd.Length; i++) - c = rnd.Next(); + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + rnd.Next(); iterationWatch.Stop(); var parser = new Md(plainMd); parseWatch.Start(); - var html = parser.Render(); + parser.Render(); parseWatch.Stop(); Console.WriteLine( 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}"; + } + + 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