From 69f2aeea0ad2b184b85e06e571753e1f188b41bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Sun, 24 Nov 2024 00:01:57 +0500 Subject: [PATCH 1/7] create project --- cs/Markdown/Converter.cs | 11 +++++++++++ cs/Markdown/Markdown.csproj | 10 ++++++++++ cs/Markdown/Md.cs | 11 +++++++++++ cs/Markdown/Preparator.cs | 11 +++++++++++ cs/Markdown/TagsTypes/BoldTag.cs | 22 ++++++++++++++++++++++ cs/Markdown/TagsTypes/HeadingTag.cs | 22 ++++++++++++++++++++++ cs/Markdown/TagsTypes/ITagsType.cs | 10 ++++++++++ cs/Markdown/TagsTypes/ItalicTag.cs | 22 ++++++++++++++++++++++ cs/Markdown/Token.cs | 21 +++++++++++++++++++++ cs/clean-code.sln | 6 ++++++ 10 files changed, 146 insertions(+) create mode 100644 cs/Markdown/Converter.cs create mode 100644 cs/Markdown/Markdown.csproj create mode 100644 cs/Markdown/Md.cs create mode 100644 cs/Markdown/Preparator.cs create mode 100644 cs/Markdown/TagsTypes/BoldTag.cs create mode 100644 cs/Markdown/TagsTypes/HeadingTag.cs create mode 100644 cs/Markdown/TagsTypes/ITagsType.cs create mode 100644 cs/Markdown/TagsTypes/ItalicTag.cs create mode 100644 cs/Markdown/Token.cs diff --git a/cs/Markdown/Converter.cs b/cs/Markdown/Converter.cs new file mode 100644 index 000000000..518dacf46 --- /dev/null +++ b/cs/Markdown/Converter.cs @@ -0,0 +1,11 @@ +namespace Markdown; + +public class Converter +{ + public string Result = ""; + + public string ConvertWithTokens(List tokens) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..2f4fc7765 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..b581f11be --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,11 @@ +namespace Markdown; + +public class Md +{ + public static string Render(string markdown) + { + Preparator preparator = new Preparator(); + Converter converter = new Converter(); + return converter.ConvertWithTokens(preparator.GetTokens(markdown)); + } +} \ No newline at end of file diff --git a/cs/Markdown/Preparator.cs b/cs/Markdown/Preparator.cs new file mode 100644 index 000000000..1a86ca823 --- /dev/null +++ b/cs/Markdown/Preparator.cs @@ -0,0 +1,11 @@ +namespace Markdown; + +public class Preparator +{ + List Tokens = new(); + + public List GetTokens(String text) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/BoldTag.cs b/cs/Markdown/TagsTypes/BoldTag.cs new file mode 100644 index 000000000..3c0b2170a --- /dev/null +++ b/cs/Markdown/TagsTypes/BoldTag.cs @@ -0,0 +1,22 @@ +namespace Markdown.TagsType; + +public class BoldTag : ITagsType +{ + public string MarkdownTag { get; } + public bool HasPairTag { get; } + public bool IsCloseTag { get; } + public bool IsNeedCloseTag { get; } + + public BoldTag(bool isCloseTag, bool hasPairTag) + { + MarkdownTag = "__"; + IsCloseTag = isCloseTag; + HasPairTag = hasPairTag; + IsNeedCloseTag = true; + } + + public string GetHtmlTag() + { + return IsCloseTag ? "" : ""; + } +} \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/HeadingTag.cs b/cs/Markdown/TagsTypes/HeadingTag.cs new file mode 100644 index 000000000..ff0e00df3 --- /dev/null +++ b/cs/Markdown/TagsTypes/HeadingTag.cs @@ -0,0 +1,22 @@ +namespace Markdown.TagsType; + +public class HeadingTag : ITagsType +{ + public string MarkdownTag { get; } + public bool HasPairTag { get; } + public bool IsCloseTag { get; } + public bool IsNeedCloseTag { get; } + + public HeadingTag(bool isCloseTag, bool hasPairTag) + { + MarkdownTag = "#"; + IsCloseTag = isCloseTag; + HasPairTag = hasPairTag; + IsNeedCloseTag = false; + } + + public string GetHtmlTag() + { + return IsCloseTag ? "" : "

"; + } +} \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/ITagsType.cs b/cs/Markdown/TagsTypes/ITagsType.cs new file mode 100644 index 000000000..088f6b0fb --- /dev/null +++ b/cs/Markdown/TagsTypes/ITagsType.cs @@ -0,0 +1,10 @@ +namespace Markdown.TagsType; + +public interface ITagsType +{ + public string MarkdownTag { get; } + public bool HasPairTag { get; } + public bool IsCloseTag { get; } + public bool IsNeedCloseTag { get; } + public string GetHtmlTag(); +} \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/ItalicTag.cs b/cs/Markdown/TagsTypes/ItalicTag.cs new file mode 100644 index 000000000..d53180aec --- /dev/null +++ b/cs/Markdown/TagsTypes/ItalicTag.cs @@ -0,0 +1,22 @@ +namespace Markdown.TagsType; + +public class ItalicTag : ITagsType +{ + public string MarkdownTag { get; } + public bool HasPairTag { get; } + public bool IsCloseTag { get; } + public bool IsNeedCloseTag { get; } + + public string GetHtmlTag() + { + return IsCloseTag ? "" : ""; + } + + public ItalicTag(bool isCloseTag, bool hasPairTag) + { + MarkdownTag = "_"; + IsCloseTag = isCloseTag; + HasPairTag = hasPairTag; + IsNeedCloseTag = true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs new file mode 100644 index 000000000..9884c2769 --- /dev/null +++ b/cs/Markdown/Token.cs @@ -0,0 +1,21 @@ +using Markdown.TagsType; + +namespace Markdown; + +public class Token +{ + public bool IsTag { get; } + public int StartIndex { get; } + public int Length { get; } + + // Если Type = null, значит этот токен для текста, иначе - для тэга. + public ITagsType? Type { get; } + + public Token(bool isTag, int startIndex, int length, ITagsType? type = null) + { + IsTag = isTag; + StartIndex = startIndex; + Length = length; + Type = type; + } +} \ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..53cf30e51 100644 --- a/cs/clean-code.sln +++ b/cs/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", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{71168D0A-8E37-49D3-B718-51AD51BDF895}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,5 +29,9 @@ 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 + {71168D0A-8E37-49D3-B718-51AD51BDF895}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71168D0A-8E37-49D3-B718-51AD51BDF895}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71168D0A-8E37-49D3-B718-51AD51BDF895}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71168D0A-8E37-49D3-B718-51AD51BDF895}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 0d90bbb5836f72ade1c07eb470d3628819abe938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Sun, 24 Nov 2024 00:07:31 +0500 Subject: [PATCH 2/7] refactor Token.cs --- cs/Markdown/Token.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 9884c2769..4067846d6 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -4,16 +4,14 @@ namespace Markdown; public class Token { - public bool IsTag { get; } public int StartIndex { get; } public int Length { get; } // Если Type = null, значит этот токен для текста, иначе - для тэга. public ITagsType? Type { get; } - public Token(bool isTag, int startIndex, int length, ITagsType? type = null) + public Token(int startIndex, int length, ITagsType? type = null) { - IsTag = isTag; StartIndex = startIndex; Length = length; Type = type; From 310cbcb9627ac05130406c214ce8fa7170884480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Wed, 4 Dec 2024 22:53:40 +0500 Subject: [PATCH 3/7] implementation of basic tags --- cs/Markdown/Converter.cs | 15 ++- cs/Markdown/Md.cs | 3 +- cs/Markdown/Parsers/BoldParser.cs | 99 +++++++++++++++++++ cs/Markdown/Parsers/GeneralParser.cs | 59 ++++++++++++ cs/Markdown/Parsers/ItalicParser.cs | 58 +++++++++++ cs/Markdown/Parsers/SlashParser.cs | 42 ++++++++ cs/Markdown/Preparator.cs | 138 ++++++++++++++++++++++++++- cs/Markdown/Program.cs | 6 ++ cs/Markdown/TagsTypes/BoldTag.cs | 17 ++-- cs/Markdown/TagsTypes/HeadingTag.cs | 17 ++-- cs/Markdown/TagsTypes/ITagsType.cs | 7 +- cs/Markdown/TagsTypes/ItalicTag.cs | 18 ++-- cs/Markdown/Token.cs | 18 ++-- 13 files changed, 444 insertions(+), 53 deletions(-) create mode 100644 cs/Markdown/Parsers/BoldParser.cs create mode 100644 cs/Markdown/Parsers/GeneralParser.cs create mode 100644 cs/Markdown/Parsers/ItalicParser.cs create mode 100644 cs/Markdown/Parsers/SlashParser.cs create mode 100644 cs/Markdown/Program.cs diff --git a/cs/Markdown/Converter.cs b/cs/Markdown/Converter.cs index 518dacf46..d6b996364 100644 --- a/cs/Markdown/Converter.cs +++ b/cs/Markdown/Converter.cs @@ -1,11 +1,18 @@ -namespace Markdown; +using System.Text; + +namespace Markdown; public class Converter { - public string Result = ""; - public string ConvertWithTokens(List tokens) { - throw new NotImplementedException(); + StringBuilder result = new(); + + foreach (var token in tokens) + { + result.Append(token.Context); + } + + return result.ToString(); } } \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index b581f11be..f216452fc 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -6,6 +6,7 @@ public static string Render(string markdown) { Preparator preparator = new Preparator(); Converter converter = new Converter(); - return converter.ConvertWithTokens(preparator.GetTokens(markdown)); + preparator.Paragrapher(markdown); + return converter.ConvertWithTokens(preparator.GetTokens()); } } \ No newline at end of file diff --git a/cs/Markdown/Parsers/BoldParser.cs b/cs/Markdown/Parsers/BoldParser.cs new file mode 100644 index 000000000..baecf83ae --- /dev/null +++ b/cs/Markdown/Parsers/BoldParser.cs @@ -0,0 +1,99 @@ +using System.Collections; +using Markdown.TagsType; + +namespace Markdown.Parsers; + +public class BoldParser +{ + private readonly Stack tagsStack; + private readonly GeneralParser.IsValidTag isValidOpenBoldTag; + private readonly GeneralParser.IsValidTag isValidCloseBoldTag; + private readonly GeneralParser.AddInsert addBoldTag; + public bool IsHaveInsideBoldTag; + public static bool IsBoldClosed; + private int index; + + public BoldParser() + { + IsBoldClosed = true; + IsHaveInsideBoldTag = false; + tagsStack = GeneralParser.tagsStack; + addBoldTag = GeneralParser.AddInsideTag; + isValidOpenBoldTag = GeneralParser.IsValidOpenTag; + isValidCloseBoldTag = GeneralParser.IsValidCloserTag; + index = 0; + } + + public Token BoldParse(string text, int i) + { + index = i; + index++; + + if (i != 0 && i != text.Length - 2) + IsHaveInsideBoldTag = true; + if (IsEmptyStringInsideBold(text, i)) + { + index = text.Length; + return new Token("____"); + } + + if (IsBoldClosed) + { + if (!isValidOpenBoldTag(text, i+1)) + { + return (new Token("__")); + } + } + else + { + if (!isValidCloseBoldTag(text, i)) + { + return new Token("__"); + } + } + + + if (tagsStack.Count > 1 && + tagsStack.Any(p => p.Type is {MarkdownTag: "_"}) && + tagsStack.Peek().Type is {MarkdownTag: "__"}) + { + tagsStack.Pop(); + } + if (!IsBoldClosed && i == text.Length - 1) + { + tagsStack.Pop(); + } + var boldTag = addBoldTag(new BoldTag(), IsBoldClosed); + IsBoldClosed = !IsBoldClosed; + + + return boldTag; + } + + public bool IsNextSymbolBold(string text, int i) + { + var nextIndex = i + 1; + if (nextIndex < text.Length) + { + if (text[nextIndex] == '_') + { + return true; + } + } + + return false; + } + + public int GetNewIndex() => index; + + private bool IsEmptyStringInsideBold(string text, int i) + { + int nextIndex = i + 1; + if (nextIndex < text.Length - 2 && IsNextSymbolBold(text, nextIndex)) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/GeneralParser.cs b/cs/Markdown/Parsers/GeneralParser.cs new file mode 100644 index 000000000..71b17f351 --- /dev/null +++ b/cs/Markdown/Parsers/GeneralParser.cs @@ -0,0 +1,59 @@ +using Markdown.TagsType; + +namespace Markdown.Parsers; + +public static class GeneralParser +{ + public static Stack tagsStack = new(); + + public delegate Token AddInsert(ITagsType tags, bool isClose); + + public delegate bool IsValidTag(string text, int index); + + public static Token AddInsideTag(ITagsType tag, bool isClose) + { + var result = GenerateTagToken(tag, isClose); + if (!isClose && tagsStack.Count != 0 && tagsStack.Peek().Type?.MarkdownTag == tag.MarkdownTag) + ClosePairTag(tagsStack.Pop()); + return result; + } + + public static Token GenerateTagToken(ITagsType tag, bool isClose) + { + if (isClose) + { + var temp = new Token(tag.GetHtmlOpenTag(), tag); + tagsStack.Push(temp); + return temp; + } + + if (tagsStack.Count != 0 && tagsStack.Peek().Type?.MarkdownTag == tag.MarkdownTag) + tag.HasPair = true; + return new Token(tag.GetHtmlCloseTag(), tag); + } + + public static void ClosePairTag(Token token) + { + if (token.Type != null) token.Type.HasPair = true; + } + + public static bool IsValidCloserTag(string text, int index) + { + if (index == 0) + return false; + if (index > 0 && text[index - 1] == ' ') + return false; + + return true; + } + + public static bool IsValidOpenTag(string text, int index) + { + if (index == text.Length - 1) + return false; + if (index < text.Length - 1 && text[index + 1] == ' ') + return false; + + return true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/ItalicParser.cs b/cs/Markdown/Parsers/ItalicParser.cs new file mode 100644 index 000000000..4ae7467a5 --- /dev/null +++ b/cs/Markdown/Parsers/ItalicParser.cs @@ -0,0 +1,58 @@ +using Markdown.TagsType; + +namespace Markdown.Parsers; + +public class ItalicParser +{ + private readonly GeneralParser.IsValidTag isValidOpenItalicTag; + private readonly GeneralParser.IsValidTag isValidCloseItalicTag; + private readonly GeneralParser.AddInsert addItalicTag; + private bool isItalicClosed; + private readonly Stack tagsStack; + public bool IsHaveInsideItalicTag; + + + public ItalicParser() + { + isItalicClosed = true; + tagsStack = GeneralParser.tagsStack; + addItalicTag = GeneralParser.AddInsideTag; + isValidOpenItalicTag = GeneralParser.IsValidOpenTag; + isValidCloseItalicTag = GeneralParser.IsValidCloserTag; + IsHaveInsideItalicTag = false; + } + + public Token ItalicParse(string text, int i) + { + if (i != 0 && i != text.Length - 1) + IsHaveInsideItalicTag = true; + if (isItalicClosed) + { + if (!isValidOpenItalicTag(text, i)) + { + return new Token("_"); + } + } + else + { + if (!isValidCloseItalicTag(text, i)) + { + return new Token("_"); + } + } + + if (tagsStack.Count > 1 && !isItalicClosed && BoldParser.IsBoldClosed) + { + tagsStack.Pop(); + } + + var result = addItalicTag(new ItalicTag(), isItalicClosed); + isItalicClosed = !isItalicClosed; + if (!isItalicClosed && i == text.Length - 1) + { + tagsStack.Pop(); + } + + return result; + } +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/SlashParser.cs b/cs/Markdown/Parsers/SlashParser.cs new file mode 100644 index 000000000..1a0c5cd7b --- /dev/null +++ b/cs/Markdown/Parsers/SlashParser.cs @@ -0,0 +1,42 @@ +using Markdown.TagsType; + +namespace Markdown.Parsers; + +public class SlashParser +{ + + private int Index; + + public SlashParser() + { + Index = 0; + } + public Token QuotedToken(string text, int i, bool isNextSymbolBold) + { + i++; + Index = i; + if (Index < text.Length) + { + switch (text[Index]) + { + case '_': + if (Index + 1 < text.Length && isNextSymbolBold) + { + Index++; + return new Token(new BoldTag().MarkdownTag); + } + return new Token(new ItalicTag().MarkdownTag); + case '#': + return new Token(new HeadingTag().MarkdownTag); + case '\\': + return new Token("\\"); + } + } + + Index--; + return new Token("\\"); + } + + public int GetIndex() => Index; + +} \ No newline at end of file diff --git a/cs/Markdown/Preparator.cs b/cs/Markdown/Preparator.cs index 1a86ca823..3937d80f2 100644 --- a/cs/Markdown/Preparator.cs +++ b/cs/Markdown/Preparator.cs @@ -1,11 +1,141 @@ -namespace Markdown; +using System.Text; +using Markdown.Parsers; +using Markdown.TagsType; + +namespace Markdown; public class Preparator { - List Tokens = new(); + private List tokens = new(); + + private readonly BoldParser boldParser; + private readonly ItalicParser italicParser; + private bool isHeadUp; + private int countOfLastEddedTokens = 0; + private readonly Stack tagsStack; + private readonly SlashParser slashParser; + + public Preparator() + { + boldParser = new BoldParser(); + italicParser = new ItalicParser(); + slashParser = new SlashParser(); + tagsStack = GeneralParser.tagsStack; + } + + public void Paragrapher(string text) + { + var paragraphs = text.Split('\n'); + foreach (var line in paragraphs) + { + Tokenize(line); + tagsStack.Clear(); + } + tokens.Remove(tokens.Last()); + CheckTokens(); + } + + + public List GetTokens() + { + return tokens; + } + + private void Tokenize(string line) + { + var words = line.Split(' '); + foreach (var token in words) + { + AddToken(token); + } + + tokens.Remove(tokens.Last()); + if (isHeadUp) + { + isHeadUp = false; + tokens.Add(new Token(new HeadingTag().GetHtmlCloseTag())); + } + + tokens.Add(new Token("\n")); + } + + + private void AddToken(string text) + { + var isHaveDigitInside = false; + + for (int i = 0; i < text.Length; i++) + { + switch (text[i]) + { + case '_': + if (boldParser.IsNextSymbolBold(text,i)) + { + tokens.Add(boldParser.BoldParse(text,i)); + i = boldParser.GetNewIndex(); + break; + } + tokens.Add(italicParser.ItalicParse(text,i)); + break; + case '#': + tokens.Add(new Token(new HeadingTag().GetHtmlOpenTag())); + isHeadUp = true; + return; + case '\\': + tokens.Add(slashParser.QuotedToken(text, i, boldParser.IsNextSymbolBold(text,i))); + i = slashParser.GetIndex(); + break; + default: + if (Char.IsDigit(text[i])) + isHaveDigitInside = true; + tokens.Add(new Token(text[i].ToString())); + break; + } + } + + tokens.Add(new Token(" ")); + countOfLastEddedTokens = tokens.Count - countOfLastEddedTokens; + GetValidResult(isHaveDigitInside, italicParser.IsHaveInsideItalicTag, boldParser.IsHaveInsideBoldTag); + } + + private void GetValidResult(bool isHaveDigitInside, bool isHaveInsideItalicTag, bool + isHaveIndsideBoldTag) + { + if (isHaveDigitInside) + MarkLastTokensToMarkdown(); + if (isHaveInsideItalicTag && tagsStack.Count != 0) + { + tagsStack.Pop(); + } + + if (isHaveIndsideBoldTag && tagsStack.Count != 0) + { + tagsStack.Pop(); + } + } + + + + private void CheckTokens() + { + foreach (var token in tokens) + { + if (token.Type != null && !token.Type.HasPair) + { + token.Context = token.Type.MarkdownTag; + } + } + } - public List GetTokens(String text) + private void MarkLastTokensToMarkdown() { - throw new NotImplementedException(); + for (int i = tokens.Count - countOfLastEddedTokens; i < tokens.Count; i++) + { + if (tokens[i].Type != null) + { + tokens[i].Type!.HasPair = false; + } + } } + } \ No newline at end of file diff --git a/cs/Markdown/Program.cs b/cs/Markdown/Program.cs new file mode 100644 index 000000000..db5f9fb88 --- /dev/null +++ b/cs/Markdown/Program.cs @@ -0,0 +1,6 @@ +// See https://aka.ms/new-console-template for more information + +using Markdown; + + +Console.WriteLine(Md.Render("Hello World!")); \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/BoldTag.cs b/cs/Markdown/TagsTypes/BoldTag.cs index 3c0b2170a..d6b526eea 100644 --- a/cs/Markdown/TagsTypes/BoldTag.cs +++ b/cs/Markdown/TagsTypes/BoldTag.cs @@ -2,21 +2,16 @@ public class BoldTag : ITagsType { - public string MarkdownTag { get; } - public bool HasPairTag { get; } - public bool IsCloseTag { get; } - public bool IsNeedCloseTag { get; } + public string MarkdownTag { get; } = "__"; + public bool HasPair { get; set; } - public BoldTag(bool isCloseTag, bool hasPairTag) + public string GetHtmlOpenTag() { - MarkdownTag = "__"; - IsCloseTag = isCloseTag; - HasPairTag = hasPairTag; - IsNeedCloseTag = true; + return ""; } - public string GetHtmlTag() + public string GetHtmlCloseTag() { - return IsCloseTag ? "" : ""; + return ""; } } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/HeadingTag.cs b/cs/Markdown/TagsTypes/HeadingTag.cs index ff0e00df3..2135feacc 100644 --- a/cs/Markdown/TagsTypes/HeadingTag.cs +++ b/cs/Markdown/TagsTypes/HeadingTag.cs @@ -2,21 +2,16 @@ public class HeadingTag : ITagsType { - public string MarkdownTag { get; } - public bool HasPairTag { get; } - public bool IsCloseTag { get; } - public bool IsNeedCloseTag { get; } + public string MarkdownTag { get; } = "#"; + public bool HasPair { get; set; } = true; - public HeadingTag(bool isCloseTag, bool hasPairTag) + public string GetHtmlOpenTag() { - MarkdownTag = "#"; - IsCloseTag = isCloseTag; - HasPairTag = hasPairTag; - IsNeedCloseTag = false; + return "

"; } - public string GetHtmlTag() + public string GetHtmlCloseTag() { - return IsCloseTag ? "

" : "

"; + return "

"; } } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/ITagsType.cs b/cs/Markdown/TagsTypes/ITagsType.cs index 088f6b0fb..0dc89eb2f 100644 --- a/cs/Markdown/TagsTypes/ITagsType.cs +++ b/cs/Markdown/TagsTypes/ITagsType.cs @@ -3,8 +3,7 @@ public interface ITagsType { public string MarkdownTag { get; } - public bool HasPairTag { get; } - public bool IsCloseTag { get; } - public bool IsNeedCloseTag { get; } - public string GetHtmlTag(); + public bool HasPair { get; set; } + public string GetHtmlOpenTag(); + public string GetHtmlCloseTag(); } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/ItalicTag.cs b/cs/Markdown/TagsTypes/ItalicTag.cs index d53180aec..6528fee5d 100644 --- a/cs/Markdown/TagsTypes/ItalicTag.cs +++ b/cs/Markdown/TagsTypes/ItalicTag.cs @@ -2,21 +2,17 @@ public class ItalicTag : ITagsType { - public string MarkdownTag { get; } - public bool HasPairTag { get; } - public bool IsCloseTag { get; } - public bool IsNeedCloseTag { get; } + public string MarkdownTag { get; } = "_"; + public bool HasPair { get; set; } - public string GetHtmlTag() + public string GetHtmlOpenTag() { - return IsCloseTag ? "
" : ""; + return ""; } - public ItalicTag(bool isCloseTag, bool hasPairTag) + public string GetHtmlCloseTag() { - MarkdownTag = "_"; - IsCloseTag = isCloseTag; - HasPairTag = hasPairTag; - IsNeedCloseTag = true; + return ""; } + } \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 4067846d6..32d0489c1 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -1,19 +1,23 @@ -using Markdown.TagsType; +using System.Collections; +using Markdown.TagsType; namespace Markdown; -public class Token +public class Token { - public int StartIndex { get; } - public int Length { get; } + public string Context { get; set; } + public int Length => Context.Length; + public static int IdCounter = 0; + public readonly int Id; // Если Type = null, значит этот токен для текста, иначе - для тэга. public ITagsType? Type { get; } - public Token(int startIndex, int length, ITagsType? type = null) + public Token(string context, ITagsType? type = null) { - StartIndex = startIndex; - Length = length; + Context = context; Type = type; + Id = IdCounter++; } + } \ No newline at end of file From ce80477f0c533b189f42de0123be9a1ab2af34e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Wed, 4 Dec 2024 22:54:11 +0500 Subject: [PATCH 4/7] Add Markdown Tests --- cs/MarkdownTests/MarkdownTest.cs | 131 ++++++++++++++++++++++++++ cs/MarkdownTests/MarkdownTests.csproj | 20 ++++ 2 files changed, 151 insertions(+) create mode 100644 cs/MarkdownTests/MarkdownTest.cs create mode 100644 cs/MarkdownTests/MarkdownTests.csproj diff --git a/cs/MarkdownTests/MarkdownTest.cs b/cs/MarkdownTests/MarkdownTest.cs new file mode 100644 index 000000000..9c8d2bd73 --- /dev/null +++ b/cs/MarkdownTests/MarkdownTest.cs @@ -0,0 +1,131 @@ +using System.Text; +using FluentAssertions; +using FluentAssertions.Extensions; +using Markdown; +using Markdown.Parsers; +using NUnit.Framework; + +namespace MarkdownTests; + +public class MarkdownTests +{ + [Test] + public void Md_ShouldReturnCorrectString_WithHeadingTag() + { + var result = Md.Render("# Thank God It's Christmas \n # Queen"); + var expected = "

Thank God It's Christmas

\n

Queen

"; + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void Md_ShouldReturnCorrectString_WithMixTags() + { + var result = Md.Render("# __Princes _of the_ Universe__"); + var expected = "

Princes of the Universe

"; + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void Md_ShouldReturnCorrectString_WithItalicInsideBoldTag() + { + var result = Md.Render("__Too Much _Love Will_ Kill You__"); + var expected = "Too Much Love Will Kill You"; + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void Md_ShouldReturnCorrectString_WithItaBoldInsideItalicTag() + { + var result = Md.Render("_We __Will Rock__ You_"); + var expected = "We __Will Rock__ You"; + result.Should().BeEquivalentTo(expected); + } + + [Test] + public void Md_ShouldReturnCorrectString_WithUnpairTagsInSameParagraph() + { + var result = Md.Render("__Another One_ Bites the Dust \n __Another _One Bites the Dust"); + var expected = "__Another One_ Bites the Dust \n __Another _One Bites the Dust"; + result.Should().BeEquivalentTo(expected); + } + + [TestCase("п_24_54", ExpectedResult = "п_24_54")] + [TestCase("п__24__54", ExpectedResult = "п__24__54")] + public string Md_ShouldReturnCorrectString_WithNumbers(string text) + { + return Md.Render(text); + } + + [TestCase("____", ExpectedResult = "____")] + [TestCase("__", ExpectedResult = "__")] + public string Md_ShouldReturnCorrectString_WithEmptyTags(string text) + { + return Md.Render(text); + } + + [TestCase("\\", ExpectedResult = "\\")] + [TestCase("\\__", ExpectedResult = "__")] + [TestCase("\\_", ExpectedResult = "_")] + [TestCase("\\#", ExpectedResult = "#")] + [TestCase("\\w", ExpectedResult = "\\w")] + public string Md_ShouldReturnCorrectString_WithSlashes(string text) + { + return Md.Render(text); + } + + [TestCase("don't_ stop_ me_ now_", ExpectedResult = "don't_ stop_ me_ now_")] + [TestCase("don't__ stop__ me__ now__", ExpectedResult = "don't__ stop__ me__ now__")] + public string Md_ShouldReturnCorrectString_WithSpacesAfterOpenTags(string text) + { + return Md.Render(text); + } + + [TestCase("_Bohemian _Rhapsody", ExpectedResult = "_Bohemian _Rhapsody")] + [TestCase("__Bohemian __Rhapsody", ExpectedResult = "__Bohemian __Rhapsody")] + public string Md_ShouldReturnCorrectString_WithSpacesBeforeCloserTags(string text) + { + return Md.Render(text); + } + + + [TestCase("__The _Show__ Must_ Go On", ExpectedResult = "__The _Show__ Must_ Go On")] + [TestCase("_The __Show_ Must__ Go On", ExpectedResult = "_The __Show_ Must__ Go On")] + public string Md_ShouldReturnCorrectString_WithCrossTags(string text) + { + return Md.Render(text); + } + + [TestCase("I Wa_nt to Br_eak Free", ExpectedResult = "I Wa_nt to Br_eak Free")] + [TestCase("I Wa__nt to Br__eak Free", ExpectedResult = "I Wa__nt to Br__eak Free")] + public string Md_ShouldReturnCorrectString_WithTagsInDifferentWords(string text) + { + return Md.Render(text); + } + + [TestCase("A K_in_d of Magic", ExpectedResult = "A Kind of Magic")] + [TestCase("A _Kin_d of Magic", ExpectedResult = "A Kind of Magic")] + [TestCase("A Kin_d_ of Magic", ExpectedResult = "A Kind of Magic")] + [TestCase("A K__in__d of Magic", ExpectedResult = "A Kind of Magic")] + [TestCase("A __Kin__d of Magic", ExpectedResult = "A Kind of Magic")] + [TestCase("A Kin__d__ of Magic", ExpectedResult = "A Kind of Magic")] + public string Md_ShouldReturnCorrectString_WithTagsInSameWords(string text) + { + return Md.Render(text); + } + + [Test] + public void Md_ShouldEnd_WithLinearTime() + { + const string markdown = "# __Princes _of the_ Universe__"; + + var renderMd = () => + { + for (var i = 0; i < 1000000; i++) + { + Md.Render(markdown); + } + }; + + renderMd.ExecutionTime().Should().BeLessThan(10.Seconds()); + } +} \ No newline at end of file diff --git a/cs/MarkdownTests/MarkdownTests.csproj b/cs/MarkdownTests/MarkdownTests.csproj new file mode 100644 index 000000000..7f2fbad5a --- /dev/null +++ b/cs/MarkdownTests/MarkdownTests.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + From 2c6ce1a404f9795b4c7a0e5191e741de6babc7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Thu, 5 Dec 2024 09:57:04 +0500 Subject: [PATCH 5/7] refactor MarkdownTest.cs --- cs/MarkdownTests/MarkdownTest.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cs/MarkdownTests/MarkdownTest.cs b/cs/MarkdownTests/MarkdownTest.cs index 9c8d2bd73..f467c0a19 100644 --- a/cs/MarkdownTests/MarkdownTest.cs +++ b/cs/MarkdownTests/MarkdownTest.cs @@ -117,14 +117,13 @@ public string Md_ShouldReturnCorrectString_WithTagsInSameWords(string text) public void Md_ShouldEnd_WithLinearTime() { const string markdown = "# __Princes _of the_ Universe__"; - - var renderMd = () => + var markdownSb = new StringBuilder(); + for (int i = 0; i < 100000; i++) { - for (var i = 0; i < 1000000; i++) - { - Md.Render(markdown); - } - }; + markdownSb.Append(markdown); + } + + var renderMd = () => { Md.Render(markdownSb.ToString()); }; renderMd.ExecutionTime().Should().BeLessThan(10.Seconds()); } From 095bdf049521243922bac4852bcbeb748f8f7a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Fri, 6 Dec 2024 17:42:49 +0500 Subject: [PATCH 6/7] Add link tag --- cs/Markdown/ListOfTokens.cs | 10 ++++++ cs/Markdown/Parsers/LinkParser.cs | 51 ++++++++++++++++++++++++++++++ cs/Markdown/Parsers/SlashParser.cs | 2 -- cs/Markdown/Preparator.cs | 17 +++++++--- cs/Markdown/TagsTypes/LinkTag.cs | 19 +++++++++++ cs/Markdown/Token.cs | 1 + cs/MarkdownTests/MarkdownTest.cs | 16 +++++++--- 7 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 cs/Markdown/ListOfTokens.cs create mode 100644 cs/Markdown/Parsers/LinkParser.cs create mode 100644 cs/Markdown/TagsTypes/LinkTag.cs diff --git a/cs/Markdown/ListOfTokens.cs b/cs/Markdown/ListOfTokens.cs new file mode 100644 index 000000000..24adbf7c5 --- /dev/null +++ b/cs/Markdown/ListOfTokens.cs @@ -0,0 +1,10 @@ +namespace Markdown; + +public class ListOfTokens : List +{ + public new void Add(T token) + { + if(!(token is null)) + base.Add(token); + } +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/LinkParser.cs b/cs/Markdown/Parsers/LinkParser.cs new file mode 100644 index 000000000..cfb99daa2 --- /dev/null +++ b/cs/Markdown/Parsers/LinkParser.cs @@ -0,0 +1,51 @@ +using System.Net.Mime; +using System.Text; +using Markdown.TagsType; + +namespace Markdown.Parsers; + +public class LinkParser +{ + private LinkTag linkTag; + private StringBuilder nameOfLink; + private StringBuilder urlOfLink; + public bool IsThisLink; + + public LinkParser() + { + linkTag = new LinkTag(); + nameOfLink = new StringBuilder(); + urlOfLink = new StringBuilder(); + } + + public Token LinkParse(string text) + { + IsThisLink = true; + for (int i = 0; i < text.Length; i++) + { + switch (text[i]) + { + case '[': + break; + case ']': + linkTag.LinkName = nameOfLink.ToString(); + linkTag.LinkUrl = text.Substring(i + 2, text.Length - i - 3); + return GenerateLinkToken(); + default: + nameOfLink.Append(text[i]); + break; + } + } + + nameOfLink.Append(' '); + return null!; + } + + private Token GenerateLinkToken() + { + var context = linkTag.GetHtmlOpenTag() + linkTag.LinkUrl + "\">" + linkTag.LinkName + linkTag.GetHtmlCloseTag(); + urlOfLink.Clear(); + nameOfLink.Clear(); + return new Token(context, linkTag); + } +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/SlashParser.cs b/cs/Markdown/Parsers/SlashParser.cs index 1a0c5cd7b..e1b17407f 100644 --- a/cs/Markdown/Parsers/SlashParser.cs +++ b/cs/Markdown/Parsers/SlashParser.cs @@ -28,8 +28,6 @@ public Token QuotedToken(string text, int i, bool isNextSymbolBold) return new Token(new ItalicTag().MarkdownTag); case '#': return new Token(new HeadingTag().MarkdownTag); - case '\\': - return new Token("\\"); } } diff --git a/cs/Markdown/Preparator.cs b/cs/Markdown/Preparator.cs index 3937d80f2..a4f50f1cd 100644 --- a/cs/Markdown/Preparator.cs +++ b/cs/Markdown/Preparator.cs @@ -6,10 +6,11 @@ namespace Markdown; public class Preparator { - private List tokens = new(); + private readonly ListOfTokens tokens = new(); private readonly BoldParser boldParser; private readonly ItalicParser italicParser; + private LinkParser linkParser; private bool isHeadUp; private int countOfLastEddedTokens = 0; private readonly Stack tagsStack; @@ -17,6 +18,7 @@ public class Preparator public Preparator() { + this.linkParser = new LinkParser(); boldParser = new BoldParser(); italicParser = new ItalicParser(); slashParser = new SlashParser(); @@ -47,6 +49,8 @@ private void Tokenize(string line) foreach (var token in words) { AddToken(token); + if(tokens.Count != 0) + tokens.Add(new Token(" ")); } tokens.Remove(tokens.Last()); @@ -63,7 +67,11 @@ private void Tokenize(string line) private void AddToken(string text) { var isHaveDigitInside = false; - + if (linkParser.IsThisLink) + { + tokens.Add(linkParser.LinkParse(text)); + return; + } for (int i = 0; i < text.Length; i++) { switch (text[i]) @@ -81,6 +89,9 @@ private void AddToken(string text) tokens.Add(new Token(new HeadingTag().GetHtmlOpenTag())); isHeadUp = true; return; + case '[': + tokens.Add(linkParser.LinkParse(text)); + return; case '\\': tokens.Add(slashParser.QuotedToken(text, i, boldParser.IsNextSymbolBold(text,i))); i = slashParser.GetIndex(); @@ -92,8 +103,6 @@ private void AddToken(string text) break; } } - - tokens.Add(new Token(" ")); countOfLastEddedTokens = tokens.Count - countOfLastEddedTokens; GetValidResult(isHaveDigitInside, italicParser.IsHaveInsideItalicTag, boldParser.IsHaveInsideBoldTag); } diff --git a/cs/Markdown/TagsTypes/LinkTag.cs b/cs/Markdown/TagsTypes/LinkTag.cs new file mode 100644 index 000000000..840ad8167 --- /dev/null +++ b/cs/Markdown/TagsTypes/LinkTag.cs @@ -0,0 +1,19 @@ +namespace Markdown.TagsType; + +public class LinkTag : ITagsType +{ + public string MarkdownTag { get; } = "[]()"; + public bool HasPair { get; set; } = true; + + public string LinkName = ""; + public string LinkUrl = ""; + public string GetHtmlOpenTag() + { + return ""; + } +} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 32d0489c1..038dfd87b 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -1,4 +1,5 @@ using System.Collections; +using Markdown.Parsers; using Markdown.TagsType; namespace Markdown; diff --git a/cs/MarkdownTests/MarkdownTest.cs b/cs/MarkdownTests/MarkdownTest.cs index f467c0a19..f1df08c08 100644 --- a/cs/MarkdownTests/MarkdownTest.cs +++ b/cs/MarkdownTests/MarkdownTest.cs @@ -13,15 +13,15 @@ public class MarkdownTests public void Md_ShouldReturnCorrectString_WithHeadingTag() { var result = Md.Render("# Thank God It's Christmas \n # Queen"); - var expected = "

Thank God It's Christmas

\n

Queen

"; + var expected = "

Thank God It's Christmas

\n

Queen

"; result.Should().BeEquivalentTo(expected); } [Test] public void Md_ShouldReturnCorrectString_WithMixTags() { - var result = Md.Render("# __Princes _of the_ Universe__"); - var expected = "

Princes of the Universe

"; + var result = Md.Render("# __Princes _of the_ Universe__ [littleLink](Yes)"); + var expected = "

Princes of the Universe littleLink

"; result.Should().BeEquivalentTo(expected); } @@ -112,7 +112,15 @@ public string Md_ShouldReturnCorrectString_WithTagsInSameWords(string text) { return Md.Render(text); } - + [TestCase("[Let's listen it](https://www.youtube.com/watch?v=-tJYN-eG1zk)", + ExpectedResult = "Let's listen it")] + [TestCase("[Queen](https://en.wikipedia.org/wiki/Queen_(band))", + ExpectedResult = "Queen")] + public string Md_ShouldReturnCorrectString_WithLink(string text) + { + return Md.Render(text); + } + [Test] public void Md_ShouldEnd_WithLinearTime() { From ba8e821b3274f9a5470aa043d64f2bdd764cf2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Thu, 12 Dec 2024 12:53:42 +0500 Subject: [PATCH 7/7] refactor project --- cs/Markdown/ListOfTokens.cs | 2 +- cs/Markdown/Md.cs | 5 +- cs/Markdown/Parsers/BoldParser.cs | 37 +++++-- cs/Markdown/Parsers/GeneralParser.cs | 4 +- cs/Markdown/Parsers/HeadingParser.cs | 17 +++ cs/Markdown/Parsers/IParser.cs | 7 ++ cs/Markdown/Parsers/ItalicParser.cs | 27 ++++- cs/Markdown/Parsers/LinkParser.cs | 16 ++- cs/Markdown/Parsers/SlashParser.cs | 52 ++++++---- cs/Markdown/Preparator.cs | 150 --------------------------- cs/Markdown/TagsTypes/BoldTag.cs | 10 +- cs/Markdown/TagsTypes/HeadingTag.cs | 10 +- cs/Markdown/TagsTypes/ITagsType.cs | 4 +- cs/Markdown/TagsTypes/ItalicTag.cs | 11 +- cs/Markdown/TagsTypes/LinkTag.cs | 10 +- cs/Markdown/TagsTypes/TextTag.cs | 9 ++ cs/Markdown/Token.cs | 9 +- cs/Markdown/TokenGenerator.cs | 149 ++++++++++++++++++++++++++ cs/MarkdownTests/MarkdownTest.cs | 17 +-- 19 files changed, 307 insertions(+), 239 deletions(-) create mode 100644 cs/Markdown/Parsers/HeadingParser.cs create mode 100644 cs/Markdown/Parsers/IParser.cs delete mode 100644 cs/Markdown/Preparator.cs create mode 100644 cs/Markdown/TagsTypes/TextTag.cs create mode 100644 cs/Markdown/TokenGenerator.cs diff --git a/cs/Markdown/ListOfTokens.cs b/cs/Markdown/ListOfTokens.cs index 24adbf7c5..3fc70ca83 100644 --- a/cs/Markdown/ListOfTokens.cs +++ b/cs/Markdown/ListOfTokens.cs @@ -4,7 +4,7 @@ public class ListOfTokens : List { public new void Add(T token) { - if(!(token is null)) + if (!(token is null)) base.Add(token); } } \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index f216452fc..bce71cd28 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -4,9 +4,8 @@ public class Md { public static string Render(string markdown) { - Preparator preparator = new Preparator(); + TokenGenerator tokenGenerator = new TokenGenerator(); Converter converter = new Converter(); - preparator.Paragrapher(markdown); - return converter.ConvertWithTokens(preparator.GetTokens()); + return converter.ConvertWithTokens(tokenGenerator.SplitParagraphs(markdown)); } } \ No newline at end of file diff --git a/cs/Markdown/Parsers/BoldParser.cs b/cs/Markdown/Parsers/BoldParser.cs index baecf83ae..59a2f2267 100644 --- a/cs/Markdown/Parsers/BoldParser.cs +++ b/cs/Markdown/Parsers/BoldParser.cs @@ -1,9 +1,10 @@ using System.Collections; +using System.Security.Principal; using Markdown.TagsType; namespace Markdown.Parsers; -public class BoldParser +public class BoldParser : IParser { private readonly Stack tagsStack; private readonly GeneralParser.IsValidTag isValidOpenBoldTag; @@ -24,7 +25,7 @@ public BoldParser() index = 0; } - public Token BoldParse(string text, int i) + private Token BoldParse(string text, int i) { index = i; index++; @@ -34,35 +35,43 @@ public Token BoldParse(string text, int i) if (IsEmptyStringInsideBold(text, i)) { index = text.Length; - return new Token("____"); + return new Token("____", new TextTag()); } if (IsBoldClosed) { - if (!isValidOpenBoldTag(text, i+1)) + if (!isValidOpenBoldTag(text, i + 1)) { - return (new Token("__")); + return (new Token("__", new TextTag())); } } else { if (!isValidCloseBoldTag(text, i)) { - return new Token("__"); + if (isValidOpenBoldTag(text, i)) + { + if (tagsStack.Count != 0) + tagsStack.Pop(); + return addBoldTag(new BoldTag(), true); + } + + return new Token("__", new TextTag()); } } - if (tagsStack.Count > 1 && tagsStack.Any(p => p.Type is {MarkdownTag: "_"}) && tagsStack.Peek().Type is {MarkdownTag: "__"}) { tagsStack.Pop(); } + if (!IsBoldClosed && i == text.Length - 1) { tagsStack.Pop(); } + var boldTag = addBoldTag(new BoldTag(), IsBoldClosed); IsBoldClosed = !IsBoldClosed; @@ -84,8 +93,6 @@ public bool IsNextSymbolBold(string text, int i) return false; } - public int GetNewIndex() => index; - private bool IsEmptyStringInsideBold(string text, int i) { int nextIndex = i + 1; @@ -96,4 +103,16 @@ private bool IsEmptyStringInsideBold(string text, int i) return false; } + + public bool TryParse(char symbol, string text, int i) + { + return symbol == '_' && IsNextSymbolBold(text, i); + } + + public Token Parse(string text, ref int i) + { + var token = BoldParse(text, i); + i = index; + return token; + } } \ No newline at end of file diff --git a/cs/Markdown/Parsers/GeneralParser.cs b/cs/Markdown/Parsers/GeneralParser.cs index 71b17f351..b7e9d3369 100644 --- a/cs/Markdown/Parsers/GeneralParser.cs +++ b/cs/Markdown/Parsers/GeneralParser.cs @@ -22,14 +22,14 @@ public static Token GenerateTagToken(ITagsType tag, bool isClose) { if (isClose) { - var temp = new Token(tag.GetHtmlOpenTag(), tag); + var temp = new Token(tag.GetHtmlOpenTag, tag); tagsStack.Push(temp); return temp; } if (tagsStack.Count != 0 && tagsStack.Peek().Type?.MarkdownTag == tag.MarkdownTag) tag.HasPair = true; - return new Token(tag.GetHtmlCloseTag(), tag); + return new Token(tag.GetHtmlCloseTag, tag); } public static void ClosePairTag(Token token) diff --git a/cs/Markdown/Parsers/HeadingParser.cs b/cs/Markdown/Parsers/HeadingParser.cs new file mode 100644 index 000000000..83edb1aba --- /dev/null +++ b/cs/Markdown/Parsers/HeadingParser.cs @@ -0,0 +1,17 @@ +using Markdown.TagsType; + +namespace Markdown.Parsers; + +public class HeadingParser : IParser +{ + public bool TryParse(char symbol, string text, int index) + { + return symbol == '#'; + } + + public Token Parse(string text, ref int index) + { + var headingTag = new HeadingTag(); + return new Token(headingTag.GetHtmlOpenTag, headingTag); + } +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/IParser.cs b/cs/Markdown/Parsers/IParser.cs new file mode 100644 index 000000000..d2295f090 --- /dev/null +++ b/cs/Markdown/Parsers/IParser.cs @@ -0,0 +1,7 @@ +namespace Markdown.Parsers; + +public interface IParser +{ + bool TryParse(char symbol, string text, int index); + Token Parse(string text, ref int index); +} \ No newline at end of file diff --git a/cs/Markdown/Parsers/ItalicParser.cs b/cs/Markdown/Parsers/ItalicParser.cs index 4ae7467a5..e85370783 100644 --- a/cs/Markdown/Parsers/ItalicParser.cs +++ b/cs/Markdown/Parsers/ItalicParser.cs @@ -2,7 +2,7 @@ namespace Markdown.Parsers; -public class ItalicParser +public class ItalicParser : IParser { private readonly GeneralParser.IsValidTag isValidOpenItalicTag; private readonly GeneralParser.IsValidTag isValidCloseItalicTag; @@ -22,7 +22,7 @@ public ItalicParser() IsHaveInsideItalicTag = false; } - public Token ItalicParse(string text, int i) + private Token ItalicParse(string text, int i) { if (i != 0 && i != text.Length - 1) IsHaveInsideItalicTag = true; @@ -30,17 +30,24 @@ public Token ItalicParse(string text, int i) { if (!isValidOpenItalicTag(text, i)) { - return new Token("_"); + return new Token("_", new TextTag()); } } else { if (!isValidCloseItalicTag(text, i)) { - return new Token("_"); + if (isValidOpenItalicTag(text, i)) + { + if (tagsStack.Count != 0) + tagsStack.Pop(); + return addItalicTag(new ItalicTag(), true); + } + + return new Token("_", new TextTag()); } } - + if (tagsStack.Count > 1 && !isItalicClosed && BoldParser.IsBoldClosed) { tagsStack.Pop(); @@ -55,4 +62,14 @@ public Token ItalicParse(string text, int i) return result; } + + public bool TryParse(char symbol, string text, int i) + { + return symbol == '_'; + } + + public Token Parse(string text, ref int index) + { + return ItalicParse(text, index); + } } \ No newline at end of file diff --git a/cs/Markdown/Parsers/LinkParser.cs b/cs/Markdown/Parsers/LinkParser.cs index cfb99daa2..96a46b914 100644 --- a/cs/Markdown/Parsers/LinkParser.cs +++ b/cs/Markdown/Parsers/LinkParser.cs @@ -4,7 +4,7 @@ namespace Markdown.Parsers; -public class LinkParser +public class LinkParser : IParser { private LinkTag linkTag; private StringBuilder nameOfLink; @@ -43,9 +43,21 @@ public Token LinkParse(string text) private Token GenerateLinkToken() { - var context = linkTag.GetHtmlOpenTag() + linkTag.LinkUrl + "\">" + linkTag.LinkName + linkTag.GetHtmlCloseTag(); + var context = linkTag.GetHtmlOpenTag + linkTag.LinkName + linkTag.GetHtmlCloseTag; urlOfLink.Clear(); nameOfLink.Clear(); return new Token(context, linkTag); } + + public bool TryParse(char symbol, string text, int i) + { + return symbol == '['; + } + + public Token Parse(string text, ref int i) + { + var token = LinkParse(text); + i = text.Length-1; + return token; + } } \ No newline at end of file diff --git a/cs/Markdown/Parsers/SlashParser.cs b/cs/Markdown/Parsers/SlashParser.cs index e1b17407f..be85dc254 100644 --- a/cs/Markdown/Parsers/SlashParser.cs +++ b/cs/Markdown/Parsers/SlashParser.cs @@ -2,39 +2,53 @@ namespace Markdown.Parsers; -public class SlashParser +public class SlashParser : IParser { + private int index; + private BoldParser boldParser; - private int Index; - - public SlashParser() + public SlashParser(BoldParser boldParser) { - Index = 0; + this.boldParser = boldParser; + index = 0; } - public Token QuotedToken(string text, int i, bool isNextSymbolBold) + + private Token QuotedToken(string text, int i, bool isNextSymbolBold) { i++; - Index = i; - if (Index < text.Length) + index = i; + if (index < text.Length) { - switch (text[Index]) + switch (text[index]) { case '_': - if (Index + 1 < text.Length && isNextSymbolBold) + if (index + 1 < text.Length && isNextSymbolBold) { - Index++; - return new Token(new BoldTag().MarkdownTag); + index++; + return new Token(new BoldTag().MarkdownTag, new TextTag()); } - return new Token(new ItalicTag().MarkdownTag); + + return new Token(new ItalicTag().MarkdownTag, new TextTag()); case '#': - return new Token(new HeadingTag().MarkdownTag); + return new Token(new HeadingTag().MarkdownTag, new TextTag()); } } - Index--; - return new Token("\\"); + index--; + return new Token("\\", new TextTag()); + } + + public int GetIndex() => index; + + public bool TryParse(char symbol, string text, int i) + { + return symbol == '\\'; + } + + public Token Parse(string text, ref int i) + { + var token = QuotedToken(text, i, boldParser.IsNextSymbolBold(text, i + 1)); + i = index; + return token; } - - public int GetIndex() => Index; - } \ No newline at end of file diff --git a/cs/Markdown/Preparator.cs b/cs/Markdown/Preparator.cs deleted file mode 100644 index a4f50f1cd..000000000 --- a/cs/Markdown/Preparator.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Text; -using Markdown.Parsers; -using Markdown.TagsType; - -namespace Markdown; - -public class Preparator -{ - private readonly ListOfTokens tokens = new(); - - private readonly BoldParser boldParser; - private readonly ItalicParser italicParser; - private LinkParser linkParser; - private bool isHeadUp; - private int countOfLastEddedTokens = 0; - private readonly Stack tagsStack; - private readonly SlashParser slashParser; - - public Preparator() - { - this.linkParser = new LinkParser(); - boldParser = new BoldParser(); - italicParser = new ItalicParser(); - slashParser = new SlashParser(); - tagsStack = GeneralParser.tagsStack; - } - - public void Paragrapher(string text) - { - var paragraphs = text.Split('\n'); - foreach (var line in paragraphs) - { - Tokenize(line); - tagsStack.Clear(); - } - tokens.Remove(tokens.Last()); - CheckTokens(); - } - - - public List GetTokens() - { - return tokens; - } - - private void Tokenize(string line) - { - var words = line.Split(' '); - foreach (var token in words) - { - AddToken(token); - if(tokens.Count != 0) - tokens.Add(new Token(" ")); - } - - tokens.Remove(tokens.Last()); - if (isHeadUp) - { - isHeadUp = false; - tokens.Add(new Token(new HeadingTag().GetHtmlCloseTag())); - } - - tokens.Add(new Token("\n")); - } - - - private void AddToken(string text) - { - var isHaveDigitInside = false; - if (linkParser.IsThisLink) - { - tokens.Add(linkParser.LinkParse(text)); - return; - } - for (int i = 0; i < text.Length; i++) - { - switch (text[i]) - { - case '_': - if (boldParser.IsNextSymbolBold(text,i)) - { - tokens.Add(boldParser.BoldParse(text,i)); - i = boldParser.GetNewIndex(); - break; - } - tokens.Add(italicParser.ItalicParse(text,i)); - break; - case '#': - tokens.Add(new Token(new HeadingTag().GetHtmlOpenTag())); - isHeadUp = true; - return; - case '[': - tokens.Add(linkParser.LinkParse(text)); - return; - case '\\': - tokens.Add(slashParser.QuotedToken(text, i, boldParser.IsNextSymbolBold(text,i))); - i = slashParser.GetIndex(); - break; - default: - if (Char.IsDigit(text[i])) - isHaveDigitInside = true; - tokens.Add(new Token(text[i].ToString())); - break; - } - } - countOfLastEddedTokens = tokens.Count - countOfLastEddedTokens; - GetValidResult(isHaveDigitInside, italicParser.IsHaveInsideItalicTag, boldParser.IsHaveInsideBoldTag); - } - - private void GetValidResult(bool isHaveDigitInside, bool isHaveInsideItalicTag, bool - isHaveIndsideBoldTag) - { - if (isHaveDigitInside) - MarkLastTokensToMarkdown(); - if (isHaveInsideItalicTag && tagsStack.Count != 0) - { - tagsStack.Pop(); - } - - if (isHaveIndsideBoldTag && tagsStack.Count != 0) - { - tagsStack.Pop(); - } - } - - - - private void CheckTokens() - { - foreach (var token in tokens) - { - if (token.Type != null && !token.Type.HasPair) - { - token.Context = token.Type.MarkdownTag; - } - } - } - - private void MarkLastTokensToMarkdown() - { - for (int i = tokens.Count - countOfLastEddedTokens; i < tokens.Count; i++) - { - if (tokens[i].Type != null) - { - tokens[i].Type!.HasPair = false; - } - } - } - -} \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/BoldTag.cs b/cs/Markdown/TagsTypes/BoldTag.cs index d6b526eea..1be81cd97 100644 --- a/cs/Markdown/TagsTypes/BoldTag.cs +++ b/cs/Markdown/TagsTypes/BoldTag.cs @@ -5,13 +5,7 @@ public class BoldTag : ITagsType public string MarkdownTag { get; } = "__"; public bool HasPair { get; set; } - public string GetHtmlOpenTag() - { - return ""; - } + public string GetHtmlOpenTag => ""; - public string GetHtmlCloseTag() - { - return ""; - } + public string GetHtmlCloseTag => ""; } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/HeadingTag.cs b/cs/Markdown/TagsTypes/HeadingTag.cs index 2135feacc..914f4e833 100644 --- a/cs/Markdown/TagsTypes/HeadingTag.cs +++ b/cs/Markdown/TagsTypes/HeadingTag.cs @@ -5,13 +5,7 @@ public class HeadingTag : ITagsType public string MarkdownTag { get; } = "#"; public bool HasPair { get; set; } = true; - public string GetHtmlOpenTag() - { - return "

"; - } + public string GetHtmlOpenTag => "

"; - public string GetHtmlCloseTag() - { - return "

"; - } + public string GetHtmlCloseTag => "

"; } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/ITagsType.cs b/cs/Markdown/TagsTypes/ITagsType.cs index 0dc89eb2f..fde21f141 100644 --- a/cs/Markdown/TagsTypes/ITagsType.cs +++ b/cs/Markdown/TagsTypes/ITagsType.cs @@ -4,6 +4,6 @@ public interface ITagsType { public string MarkdownTag { get; } public bool HasPair { get; set; } - public string GetHtmlOpenTag(); - public string GetHtmlCloseTag(); + public string GetHtmlOpenTag { get; } + public string GetHtmlCloseTag { get; } } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/ItalicTag.cs b/cs/Markdown/TagsTypes/ItalicTag.cs index 6528fee5d..d0b2486a2 100644 --- a/cs/Markdown/TagsTypes/ItalicTag.cs +++ b/cs/Markdown/TagsTypes/ItalicTag.cs @@ -5,14 +5,7 @@ public class ItalicTag : ITagsType public string MarkdownTag { get; } = "_"; public bool HasPair { get; set; } - public string GetHtmlOpenTag() - { - return ""; - } + public string GetHtmlOpenTag => ""; - public string GetHtmlCloseTag() - { - return ""; - } - + public string GetHtmlCloseTag => ""; } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/LinkTag.cs b/cs/Markdown/TagsTypes/LinkTag.cs index 840ad8167..833a96326 100644 --- a/cs/Markdown/TagsTypes/LinkTag.cs +++ b/cs/Markdown/TagsTypes/LinkTag.cs @@ -7,13 +7,7 @@ public class LinkTag : ITagsType public string LinkName = ""; public string LinkUrl = ""; - public string GetHtmlOpenTag() - { - return " $""; - public string GetHtmlCloseTag() - { - return ""; - } + public string GetHtmlCloseTag => ""; } \ No newline at end of file diff --git a/cs/Markdown/TagsTypes/TextTag.cs b/cs/Markdown/TagsTypes/TextTag.cs new file mode 100644 index 000000000..bef196f3f --- /dev/null +++ b/cs/Markdown/TagsTypes/TextTag.cs @@ -0,0 +1,9 @@ +namespace Markdown.TagsType; + +public class TextTag : ITagsType +{ + public string MarkdownTag { get; } + public bool HasPair { get; set; } + public string GetHtmlOpenTag { get; } + public string GetHtmlCloseTag { get; } +} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 038dfd87b..20dbe66b1 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -4,21 +4,18 @@ namespace Markdown; -public class Token +public class Token { public string Context { get; set; } public int Length => Context.Length; public static int IdCounter = 0; public readonly int Id; + public ITagsType Type { get; } - // Если Type = null, значит этот токен для текста, иначе - для тэга. - public ITagsType? Type { get; } - - public Token(string context, ITagsType? type = null) + public Token(string context, ITagsType type) { Context = context; Type = type; Id = IdCounter++; } - } \ No newline at end of file diff --git a/cs/Markdown/TokenGenerator.cs b/cs/Markdown/TokenGenerator.cs new file mode 100644 index 000000000..15efbde55 --- /dev/null +++ b/cs/Markdown/TokenGenerator.cs @@ -0,0 +1,149 @@ +using System.Text; +using Markdown.Parsers; +using Markdown.TagsType; + +namespace Markdown; + +public class TokenGenerator +{ + private BoldParser boldParser; + private ItalicParser italicParser; + private LinkParser linkParser; + + private readonly Stack tagsStack; + private SlashParser slashParser; + private readonly List parsers; + + public TokenGenerator() + { + boldParser = new BoldParser(); + linkParser = new LinkParser(); + italicParser = new ItalicParser(); + var headingParser = new HeadingParser(); + slashParser = new SlashParser(boldParser); + parsers = new List + { + boldParser, + headingParser, + linkParser, + slashParser, + italicParser + }; + + tagsStack = GeneralParser.tagsStack; + } + + public List SplitParagraphs(string text) + { + ListOfTokens tokens = new(); + + var paragraphs = text.Split("\r\n"); + foreach (var line in paragraphs) + { + Tokenize(line, tokens); + tagsStack.Clear(); + } + + tokens.Remove(tokens.Last()); + ConvertTokensWithOutPairToMarkdown(tokens); + linkParser = new LinkParser(); + boldParser = new BoldParser(); + italicParser = new ItalicParser(); + slashParser = new SlashParser(boldParser); + return tokens; + } + + private void Tokenize(string line, ListOfTokens tokens) + { + var lastAddIndex = tokens.Count == 0 ? 0 : tokens.Count() + 1; + var words = line.Split(' '); + foreach (var word in words) + { + AddToken(word, tokens); + if (tokens.Count != 0) + tokens.Add(new Token(" ", new TextTag())); + } + + tokens.Remove(tokens.Last()); + if (lastAddIndex > -1 && tokens[lastAddIndex].Context == new HeadingTag().GetHtmlOpenTag) + { + var headingTag = new HeadingTag(); + tokens.Add(new Token(headingTag.GetHtmlCloseTag, headingTag)); + } + + tokens.Add(new Token("\n", new TextTag())); + } + + private void AddToken(string text, ListOfTokens tokens) + { + var countOfLastAddedTokens = tokens.Count(); + var isHaveDigitInside = false; + if (linkParser.IsThisLink) + { + tokens.Add(linkParser.LinkParse(text)); + return; + } + + for (int i = 0; i < text.Length; i++) + { + bool match = false; + + foreach (var parser in parsers) + { + if (parser.TryParse(text[i], text, i)) + { + match = true; + tokens.Add(parser.Parse(text, ref i)); + break; + } + } + + if (!match) + { + if (Char.IsDigit(text[i])) + isHaveDigitInside = true; + tokens.Add(new Token(text[i].ToString(), new TextTag())); + } + } + + countOfLastAddedTokens = tokens.Count - countOfLastAddedTokens; + GetValidResult(isHaveDigitInside, tokens, countOfLastAddedTokens); + } + + private void GetValidResult(bool isHaveDigitInside, ListOfTokens tokens, int countOfLastAddedTokens) + { + if (isHaveDigitInside) + MarkLastTokensToMarkdown(tokens, countOfLastAddedTokens); + if (italicParser.IsHaveInsideItalicTag && tagsStack.Count != 0) + { + tagsStack.Pop(); + } + + if (boldParser.IsHaveInsideBoldTag && tagsStack.Count != 0) + { + tagsStack.Pop(); + } + } + + private void ConvertTokensWithOutPairToMarkdown(ListOfTokens tokens) + { + foreach (var token in tokens) + { + if (token.Type is not TextTag && !token.Type.HasPair) + { + token.Context = token.Type.MarkdownTag; + } + } + } + + private void MarkLastTokensToMarkdown(ListOfTokens tokens, int countOfLastAddedTokens) + { + for (int i = tokens.Count - countOfLastAddedTokens; i < tokens.Count; i++) + { + if (tokens[i].Type is not TextTag) + { + tokens[i].Type!.HasPair = false; + } + } + } +} \ No newline at end of file diff --git a/cs/MarkdownTests/MarkdownTest.cs b/cs/MarkdownTests/MarkdownTest.cs index f1df08c08..db9ec6d8c 100644 --- a/cs/MarkdownTests/MarkdownTest.cs +++ b/cs/MarkdownTests/MarkdownTest.cs @@ -2,7 +2,6 @@ using FluentAssertions; using FluentAssertions.Extensions; using Markdown; -using Markdown.Parsers; using NUnit.Framework; namespace MarkdownTests; @@ -12,7 +11,7 @@ public class MarkdownTests [Test] public void Md_ShouldReturnCorrectString_WithHeadingTag() { - var result = Md.Render("# Thank God It's Christmas \n # Queen"); + var result = Md.Render("# Thank God It's Christmas \r\n # Queen"); var expected = "

Thank God It's Christmas

\n

Queen

"; result.Should().BeEquivalentTo(expected); } @@ -68,6 +67,9 @@ public string Md_ShouldReturnCorrectString_WithEmptyTags(string text) [TestCase("\\_", ExpectedResult = "_")] [TestCase("\\#", ExpectedResult = "#")] [TestCase("\\w", ExpectedResult = "\\w")] + [TestCase("\\# I want to break free", ExpectedResult = "# I want to break free")] + [TestCase("\\# I \\__wan\\__t \\_to\\_ break free", ExpectedResult = "# I __wan__t _to_ break free")] + public string Md_ShouldReturnCorrectString_WithSlashes(string text) { return Md.Render(text); @@ -80,8 +82,8 @@ public string Md_ShouldReturnCorrectString_WithSpacesAfterOpenTags(string text) return Md.Render(text); } - [TestCase("_Bohemian _Rhapsody", ExpectedResult = "_Bohemian _Rhapsody")] - [TestCase("__Bohemian __Rhapsody", ExpectedResult = "__Bohemian __Rhapsody")] + [TestCase("_Bohemian _Rhapsody by Queen_", ExpectedResult = "_Bohemian Rhapsody by Queen")] + [TestCase("__Bohemian __Rhapsody by Queen__", ExpectedResult = "__Bohemian Rhapsody by Queen")] public string Md_ShouldReturnCorrectString_WithSpacesBeforeCloserTags(string text) { return Md.Render(text); @@ -112,15 +114,16 @@ public string Md_ShouldReturnCorrectString_WithTagsInSameWords(string text) { return Md.Render(text); } - [TestCase("[Let's listen it](https://www.youtube.com/watch?v=-tJYN-eG1zk)", + + [TestCase("[Let's listen it](https://www.youtube.com/watch?v=-tJYN-eG1zk)", ExpectedResult = "Let's listen it")] - [TestCase("[Queen](https://en.wikipedia.org/wiki/Queen_(band))", + [TestCase("[Queen](https://en.wikipedia.org/wiki/Queen_(band))", ExpectedResult = "Queen")] public string Md_ShouldReturnCorrectString_WithLink(string text) { return Md.Render(text); } - + [Test] public void Md_ShouldEnd_WithLinearTime() {