From 882da40081dfe51ea73865b2701e8a87bc847cf2 Mon Sep 17 00:00:00 2001 From: Maxim Date: Tue, 19 Nov 2024 01:08:21 +0500 Subject: [PATCH 01/13] Project setup --- cs/Markdown/Markdown.csproj | 10 ++++++++++ cs/Markdown/Md.cs | 6 ++++++ cs/Markdown/Program.cs | 9 +++++++++ cs/clean-code.sln | 6 ++++++ cs/clean-code.sln.DotSettings | 3 +++ 5 files changed, 34 insertions(+) create mode 100644 cs/Markdown/Markdown.csproj create mode 100644 cs/Markdown/Md.cs create mode 100644 cs/Markdown/Program.cs 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..9b4ee5955 --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,6 @@ +namespace Markdown; + +public class Md +{ + public string Render(string text) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/cs/Markdown/Program.cs b/cs/Markdown/Program.cs new file mode 100644 index 000000000..08a2138cf --- /dev/null +++ b/cs/Markdown/Program.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +class Program +{ + public static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } +} \ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..a96bc5702 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", "{71910566-C5DE-4C56-93E4-16976D5D4FC6}" +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 + {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/cs/clean-code.sln.DotSettings b/cs/clean-code.sln.DotSettings index 135b83ecb..229f449d2 100644 --- a/cs/clean-code.sln.DotSettings +++ b/cs/clean-code.sln.DotSettings @@ -1,6 +1,9 @@  <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016 From f5c3f1d23d73116c16b2a2311e8789174297a833 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 24 Nov 2024 20:08:32 +0500 Subject: [PATCH 02/13] Initial architecture --- cs/Markdown/HtmlConverter.cs | 8 ++++++++ cs/Markdown/IConverter.cs | 8 ++++++++ cs/Markdown/Md.cs | 11 +++++++++-- cs/Markdown/Tags/BoldTag.cs | 7 +++++++ cs/Markdown/Tags/HeaderTag.cs | 7 +++++++ cs/Markdown/Tags/ITag.cs | 10 ++++++++++ cs/Markdown/Tags/ItalicTag.cs | 9 +++++++++ cs/Markdown/Tags/PairTag.cs | 12 ++++++++++++ cs/Markdown/Tags/SingleTag.cs | 10 ++++++++++ cs/Markdown/Tags/TagStatus.cs | 7 +++++++ cs/Markdown/Tokenizer.cs | 9 +++++++++ cs/Markdown/Tokens/EscapeToken.cs | 7 +++++++ cs/Markdown/Tokens/IToken.cs | 7 +++++++ cs/Markdown/Tokens/NewLineToken.cs | 7 +++++++ cs/Markdown/Tokens/TagToken.cs | 13 +++++++++++++ cs/Markdown/Tokens/TextToken.cs | 7 +++++++ 16 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 cs/Markdown/HtmlConverter.cs create mode 100644 cs/Markdown/IConverter.cs create mode 100644 cs/Markdown/Tags/BoldTag.cs create mode 100644 cs/Markdown/Tags/HeaderTag.cs create mode 100644 cs/Markdown/Tags/ITag.cs create mode 100644 cs/Markdown/Tags/ItalicTag.cs create mode 100644 cs/Markdown/Tags/PairTag.cs create mode 100644 cs/Markdown/Tags/SingleTag.cs create mode 100644 cs/Markdown/Tags/TagStatus.cs create mode 100644 cs/Markdown/Tokenizer.cs create mode 100644 cs/Markdown/Tokens/EscapeToken.cs create mode 100644 cs/Markdown/Tokens/IToken.cs create mode 100644 cs/Markdown/Tokens/NewLineToken.cs create mode 100644 cs/Markdown/Tokens/TagToken.cs create mode 100644 cs/Markdown/Tokens/TextToken.cs diff --git a/cs/Markdown/HtmlConverter.cs b/cs/Markdown/HtmlConverter.cs new file mode 100644 index 000000000..891e8b955 --- /dev/null +++ b/cs/Markdown/HtmlConverter.cs @@ -0,0 +1,8 @@ +using Markdown.Tokens; + +namespace Markdown; + +public class HtmlConverter : IConverter +{ + public string Convert(List tokens) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/cs/Markdown/IConverter.cs b/cs/Markdown/IConverter.cs new file mode 100644 index 000000000..378315b3d --- /dev/null +++ b/cs/Markdown/IConverter.cs @@ -0,0 +1,8 @@ +using Markdown.Tokens; + +namespace Markdown; + +public interface IConverter +{ + string Convert(List tokens); +} \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 9b4ee5955..f9f5ba435 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -1,6 +1,13 @@ +using Markdown.Tags; + namespace Markdown; -public class Md +public static class Md { - public string Render(string text) => throw new NotImplementedException(); + private static readonly IEnumerable Tags = [new BoldTag(), new HeaderTag(), new ItalicTag()]; + public static string Render(string markdownText) + { + var tokens = new Tokenizer(Tags).Tokenize(markdownText); + return new HtmlConverter().Convert(tokens); + } } \ No newline at end of file diff --git a/cs/Markdown/Tags/BoldTag.cs b/cs/Markdown/Tags/BoldTag.cs new file mode 100644 index 000000000..72720a907 --- /dev/null +++ b/cs/Markdown/Tags/BoldTag.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tags; + +public class BoldTag : PairTag, ITag +{ + public override string MdTag => "__"; + public override string HtmlTag => ""; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/HeaderTag.cs b/cs/Markdown/Tags/HeaderTag.cs new file mode 100644 index 000000000..33be2bd30 --- /dev/null +++ b/cs/Markdown/Tags/HeaderTag.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tags; + +public class HeaderTag : SingleTag, ITag +{ + public override string MdTag => "#"; + public override string HtmlTag => "

"; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/ITag.cs b/cs/Markdown/Tags/ITag.cs new file mode 100644 index 000000000..50b86c39e --- /dev/null +++ b/cs/Markdown/Tags/ITag.cs @@ -0,0 +1,10 @@ +namespace Markdown.Tags; + +public interface ITag +{ + string MdTag { get; } + string HtmlTag { get; } + + bool IsOpenedCorrectly((char left, char right) contextChars); + bool IsClosedCorrectly((char left, char right) contextChars); +} \ No newline at end of file diff --git a/cs/Markdown/Tags/ItalicTag.cs b/cs/Markdown/Tags/ItalicTag.cs new file mode 100644 index 000000000..7bff44801 --- /dev/null +++ b/cs/Markdown/Tags/ItalicTag.cs @@ -0,0 +1,9 @@ +namespace Markdown.Tags; + +public class ItalicTag : PairTag, ITag +{ + public override string MdTag => "_"; + public override string HtmlTag => ""; + + protected override IEnumerable ForbiddenInside => [new BoldTag()]; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/PairTag.cs b/cs/Markdown/Tags/PairTag.cs new file mode 100644 index 000000000..df0f6cab9 --- /dev/null +++ b/cs/Markdown/Tags/PairTag.cs @@ -0,0 +1,12 @@ +namespace Markdown.Tags; + +public abstract class PairTag : ITag +{ + public abstract string MdTag { get; } + public abstract string HtmlTag { get; } + + public virtual bool IsOpenedCorrectly((char left, char right) contextChars) => contextChars.left != ' '; + public virtual bool IsClosedCorrectly((char left, char right) contextChars) => contextChars.right != ' '; + + protected virtual IEnumerable ForbiddenInside => []; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/SingleTag.cs b/cs/Markdown/Tags/SingleTag.cs new file mode 100644 index 000000000..6db6aefe2 --- /dev/null +++ b/cs/Markdown/Tags/SingleTag.cs @@ -0,0 +1,10 @@ +namespace Markdown.Tags; + +public abstract class SingleTag : ITag +{ + public abstract string MdTag { get; } + public abstract string HtmlTag { get; } + + public virtual bool IsOpenedCorrectly((char left, char right) contextChars) => contextChars.left == '\n'; + public bool IsClosedCorrectly((char left, char right) contextChars) => true; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/TagStatus.cs b/cs/Markdown/Tags/TagStatus.cs new file mode 100644 index 000000000..ddfa035d0 --- /dev/null +++ b/cs/Markdown/Tags/TagStatus.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tags; + +public enum TagStatus +{ + Opened, + Closed +} \ No newline at end of file diff --git a/cs/Markdown/Tokenizer.cs b/cs/Markdown/Tokenizer.cs new file mode 100644 index 000000000..ff86f5142 --- /dev/null +++ b/cs/Markdown/Tokenizer.cs @@ -0,0 +1,9 @@ +using Markdown.Tags; +using Markdown.Tokens; + +namespace Markdown; + +public class Tokenizer(IEnumerable tags) +{ + public List Tokenize(string markdownText) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/EscapeToken.cs b/cs/Markdown/Tokens/EscapeToken.cs new file mode 100644 index 000000000..d22b14ec2 --- /dev/null +++ b/cs/Markdown/Tokens/EscapeToken.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tokens; + +public class EscapeToken : IToken +{ + public string Value => "\\"; + public int Length => 1; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/IToken.cs b/cs/Markdown/Tokens/IToken.cs new file mode 100644 index 000000000..58b7492bb --- /dev/null +++ b/cs/Markdown/Tokens/IToken.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tokens; + +public interface IToken +{ + string Value { get; } + int Length { get; } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/NewLineToken.cs b/cs/Markdown/Tokens/NewLineToken.cs new file mode 100644 index 000000000..ade1fe7b4 --- /dev/null +++ b/cs/Markdown/Tokens/NewLineToken.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tokens; + +public class NewLineToken : IToken +{ + public string Value => "\n"; + public int Length => 1; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/TagToken.cs b/cs/Markdown/Tokens/TagToken.cs new file mode 100644 index 000000000..da60426ca --- /dev/null +++ b/cs/Markdown/Tokens/TagToken.cs @@ -0,0 +1,13 @@ +using Markdown.Tags; + +namespace Markdown.Tokens; + +public class TagToken(ITag tag, char left, char right) : IToken +{ + public ITag Tag => tag; + public TagStatus Status { get; set; } + // TODO: we should get value of tag by his status? + public string Value => throw new NotImplementedException(); + public int Length => Tag.MdTag.Length; + public (char left, char right) ContextChars => (left, right); +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/TextToken.cs b/cs/Markdown/Tokens/TextToken.cs new file mode 100644 index 000000000..23c7c9515 --- /dev/null +++ b/cs/Markdown/Tokens/TextToken.cs @@ -0,0 +1,7 @@ +namespace Markdown.Tokens; + +public class TextToken(string value) : IToken +{ + public string Value => value; + public int Length => Value.Length; +} \ No newline at end of file From 18b7b96ef6e55cb59ddb91a06f7549b42d215d51 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 24 Nov 2024 21:26:32 +0500 Subject: [PATCH 03/13] Added test project for Markdown --- cs/MarkdownTests/MarkdownTests.csproj | 29 +++++++++++++++++++++++++++ cs/clean-code.sln | 6 ++++++ 2 files changed, 35 insertions(+) create mode 100644 cs/MarkdownTests/MarkdownTests.csproj diff --git a/cs/MarkdownTests/MarkdownTests.csproj b/cs/MarkdownTests/MarkdownTests.csproj new file mode 100644 index 000000000..4f4a35e3f --- /dev/null +++ b/cs/MarkdownTests/MarkdownTests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/cs/clean-code.sln b/cs/clean-code.sln index a96bc5702..8b231a313 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{71910566-C5DE-4C56-93E4-16976D5D4FC6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownTests", "MarkdownTests\MarkdownTests.csproj", "{742ED593-BF23-4F69-BD7C-255777424108}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,5 +35,9 @@ Global {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Debug|Any CPU.Build.0 = Debug|Any CPU {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Release|Any CPU.ActiveCfg = Release|Any CPU {71910566-C5DE-4C56-93E4-16976D5D4FC6}.Release|Any CPU.Build.0 = Release|Any CPU + {742ED593-BF23-4F69-BD7C-255777424108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {742ED593-BF23-4F69-BD7C-255777424108}.Debug|Any CPU.Build.0 = Debug|Any CPU + {742ED593-BF23-4F69-BD7C-255777424108}.Release|Any CPU.ActiveCfg = Release|Any CPU + {742ED593-BF23-4F69-BD7C-255777424108}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 898ec46987e9a06b8d6ddffcb2373e452e003155 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 24 Nov 2024 23:18:59 +0500 Subject: [PATCH 04/13] remove unnecessary interface references --- cs/Markdown/Tags/BoldTag.cs | 2 +- cs/Markdown/Tags/HeaderTag.cs | 2 +- cs/Markdown/Tags/ItalicTag.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cs/Markdown/Tags/BoldTag.cs b/cs/Markdown/Tags/BoldTag.cs index 72720a907..c0cbf677a 100644 --- a/cs/Markdown/Tags/BoldTag.cs +++ b/cs/Markdown/Tags/BoldTag.cs @@ -1,6 +1,6 @@ namespace Markdown.Tags; -public class BoldTag : PairTag, ITag +public class BoldTag : PairTag { public override string MdTag => "__"; public override string HtmlTag => ""; diff --git a/cs/Markdown/Tags/HeaderTag.cs b/cs/Markdown/Tags/HeaderTag.cs index 33be2bd30..647bf305c 100644 --- a/cs/Markdown/Tags/HeaderTag.cs +++ b/cs/Markdown/Tags/HeaderTag.cs @@ -1,6 +1,6 @@ namespace Markdown.Tags; -public class HeaderTag : SingleTag, ITag +public class HeaderTag : SingleTag { public override string MdTag => "#"; public override string HtmlTag => "

"; diff --git a/cs/Markdown/Tags/ItalicTag.cs b/cs/Markdown/Tags/ItalicTag.cs index 7bff44801..b793a3fa7 100644 --- a/cs/Markdown/Tags/ItalicTag.cs +++ b/cs/Markdown/Tags/ItalicTag.cs @@ -1,6 +1,6 @@ namespace Markdown.Tags; -public class ItalicTag : PairTag, ITag +public class ItalicTag : PairTag { public override string MdTag => "_"; public override string HtmlTag => ""; From a05ef8a141dc4e4844893a1a1f0f85a06454c89b Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 24 Nov 2024 23:22:37 +0500 Subject: [PATCH 05/13] refactor Converter to MdConverter --- cs/Markdown/{HtmlConverter.cs => HtmlMdConverter.cs} | 2 +- cs/Markdown/{IConverter.cs => IMdConverter.cs} | 2 +- cs/Markdown/Md.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename cs/Markdown/{HtmlConverter.cs => HtmlMdConverter.cs} (75%) rename cs/Markdown/{IConverter.cs => IMdConverter.cs} (74%) diff --git a/cs/Markdown/HtmlConverter.cs b/cs/Markdown/HtmlMdConverter.cs similarity index 75% rename from cs/Markdown/HtmlConverter.cs rename to cs/Markdown/HtmlMdConverter.cs index 891e8b955..2b785eb81 100644 --- a/cs/Markdown/HtmlConverter.cs +++ b/cs/Markdown/HtmlMdConverter.cs @@ -2,7 +2,7 @@ namespace Markdown; -public class HtmlConverter : IConverter +public class HtmlMdConverter : IMdConverter { public string Convert(List tokens) => throw new NotImplementedException(); } \ No newline at end of file diff --git a/cs/Markdown/IConverter.cs b/cs/Markdown/IMdConverter.cs similarity index 74% rename from cs/Markdown/IConverter.cs rename to cs/Markdown/IMdConverter.cs index 378315b3d..bab454f53 100644 --- a/cs/Markdown/IConverter.cs +++ b/cs/Markdown/IMdConverter.cs @@ -2,7 +2,7 @@ namespace Markdown; -public interface IConverter +public interface IMdConverter { string Convert(List tokens); } \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index f9f5ba435..f80f6f152 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -8,6 +8,6 @@ public static class Md public static string Render(string markdownText) { var tokens = new Tokenizer(Tags).Tokenize(markdownText); - return new HtmlConverter().Convert(tokens); + return new HtmlMdConverter().Convert(tokens); } } \ No newline at end of file From 23b8f6ad8f5b4f7419ddd22d790527aa17279827 Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 25 Nov 2024 11:20:52 +0500 Subject: [PATCH 06/13] now we distinguish PairTag and SingleTag --- cs/Markdown/Tags/BoldTag.cs | 5 +++-- cs/Markdown/Tags/ContextString.cs | 3 +++ cs/Markdown/Tags/HeaderTag.cs | 4 ++-- cs/Markdown/Tags/ITag.cs | 9 ++++++--- cs/Markdown/Tags/ItalicTag.cs | 8 ++++---- cs/Markdown/Tags/PairTag.cs | 11 ++++++----- cs/Markdown/Tags/SingleTag.cs | 9 ++++++--- cs/Markdown/Tokens/EscapeToken.cs | 1 - cs/Markdown/Tokens/IToken.cs | 1 - cs/Markdown/Tokens/NewLineToken.cs | 1 - cs/Markdown/Tokens/TagToken.cs | 5 ++--- cs/Markdown/Tokens/TextToken.cs | 1 - 12 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 cs/Markdown/Tags/ContextString.cs diff --git a/cs/Markdown/Tags/BoldTag.cs b/cs/Markdown/Tags/BoldTag.cs index c0cbf677a..be4cd2270 100644 --- a/cs/Markdown/Tags/BoldTag.cs +++ b/cs/Markdown/Tags/BoldTag.cs @@ -2,6 +2,7 @@ namespace Markdown.Tags; public class BoldTag : PairTag { - public override string MdTag => "__"; - public override string HtmlTag => ""; + public override string MdOpenTag => "__"; + public override string MdCloseTag => MdOpenTag; + public override string HtmlTag => "strong"; } \ No newline at end of file diff --git a/cs/Markdown/Tags/ContextString.cs b/cs/Markdown/Tags/ContextString.cs new file mode 100644 index 000000000..bf9bf565b --- /dev/null +++ b/cs/Markdown/Tags/ContextString.cs @@ -0,0 +1,3 @@ +namespace Markdown.Tags; + +public record ContextString(string Left, string Right); \ No newline at end of file diff --git a/cs/Markdown/Tags/HeaderTag.cs b/cs/Markdown/Tags/HeaderTag.cs index 647bf305c..868c80c8b 100644 --- a/cs/Markdown/Tags/HeaderTag.cs +++ b/cs/Markdown/Tags/HeaderTag.cs @@ -2,6 +2,6 @@ namespace Markdown.Tags; public class HeaderTag : SingleTag { - public override string MdTag => "#"; - public override string HtmlTag => "

"; + public override string MdOpenTag => "# "; + public override string HtmlTag => "h1"; } \ No newline at end of file diff --git a/cs/Markdown/Tags/ITag.cs b/cs/Markdown/Tags/ITag.cs index 50b86c39e..0a2b60a00 100644 --- a/cs/Markdown/Tags/ITag.cs +++ b/cs/Markdown/Tags/ITag.cs @@ -2,9 +2,12 @@ namespace Markdown.Tags; public interface ITag { - string MdTag { get; } + string MdOpenTag { get; } + string? MdCloseTag { get; } string HtmlTag { get; } + bool SelfClosingTag { get; } + IEnumerable ForbiddenInside { get; } - bool IsOpenedCorrectly((char left, char right) contextChars); - bool IsClosedCorrectly((char left, char right) contextChars); + bool IsOpenedCorrectly(ContextString ctx); + bool IsClosedCorrectly(ContextString ctx); } \ No newline at end of file diff --git a/cs/Markdown/Tags/ItalicTag.cs b/cs/Markdown/Tags/ItalicTag.cs index b793a3fa7..5a1aa633e 100644 --- a/cs/Markdown/Tags/ItalicTag.cs +++ b/cs/Markdown/Tags/ItalicTag.cs @@ -2,8 +2,8 @@ namespace Markdown.Tags; public class ItalicTag : PairTag { - public override string MdTag => "_"; - public override string HtmlTag => ""; - - protected override IEnumerable ForbiddenInside => [new BoldTag()]; + public override string MdOpenTag => "_"; + public override string MdCloseTag => MdOpenTag; + public override string HtmlTag => "em"; + public override IEnumerable ForbiddenInside => [new BoldTag()]; } \ No newline at end of file diff --git a/cs/Markdown/Tags/PairTag.cs b/cs/Markdown/Tags/PairTag.cs index df0f6cab9..3918cb506 100644 --- a/cs/Markdown/Tags/PairTag.cs +++ b/cs/Markdown/Tags/PairTag.cs @@ -2,11 +2,12 @@ namespace Markdown.Tags; public abstract class PairTag : ITag { - public abstract string MdTag { get; } + public abstract string MdOpenTag { get; } + public abstract string MdCloseTag { get; } public abstract string HtmlTag { get; } + public bool SelfClosingTag => false; + public virtual IEnumerable ForbiddenInside => []; - public virtual bool IsOpenedCorrectly((char left, char right) contextChars) => contextChars.left != ' '; - public virtual bool IsClosedCorrectly((char left, char right) contextChars) => contextChars.right != ' '; - - protected virtual IEnumerable ForbiddenInside => []; + public virtual bool IsOpenedCorrectly(ContextString ctx) => ctx.Left.First() != ' '; + public virtual bool IsClosedCorrectly(ContextString ctx) => ctx.Right.First() != ' '; } \ No newline at end of file diff --git a/cs/Markdown/Tags/SingleTag.cs b/cs/Markdown/Tags/SingleTag.cs index 6db6aefe2..428bb6048 100644 --- a/cs/Markdown/Tags/SingleTag.cs +++ b/cs/Markdown/Tags/SingleTag.cs @@ -2,9 +2,12 @@ namespace Markdown.Tags; public abstract class SingleTag : ITag { - public abstract string MdTag { get; } + public abstract string MdOpenTag { get; } + public string? MdCloseTag => null; public abstract string HtmlTag { get; } + public bool SelfClosingTag => true; + public virtual IEnumerable ForbiddenInside => []; - public virtual bool IsOpenedCorrectly((char left, char right) contextChars) => contextChars.left == '\n'; - public bool IsClosedCorrectly((char left, char right) contextChars) => true; + public virtual bool IsOpenedCorrectly(ContextString ctx) => ctx.Left.Contains('\n'); + public bool IsClosedCorrectly(ContextString ctx) => true; } \ No newline at end of file diff --git a/cs/Markdown/Tokens/EscapeToken.cs b/cs/Markdown/Tokens/EscapeToken.cs index d22b14ec2..f7e7e63c9 100644 --- a/cs/Markdown/Tokens/EscapeToken.cs +++ b/cs/Markdown/Tokens/EscapeToken.cs @@ -3,5 +3,4 @@ namespace Markdown.Tokens; public class EscapeToken : IToken { public string Value => "\\"; - public int Length => 1; } \ No newline at end of file diff --git a/cs/Markdown/Tokens/IToken.cs b/cs/Markdown/Tokens/IToken.cs index 58b7492bb..e72de6a68 100644 --- a/cs/Markdown/Tokens/IToken.cs +++ b/cs/Markdown/Tokens/IToken.cs @@ -3,5 +3,4 @@ namespace Markdown.Tokens; public interface IToken { string Value { get; } - int Length { get; } } \ No newline at end of file diff --git a/cs/Markdown/Tokens/NewLineToken.cs b/cs/Markdown/Tokens/NewLineToken.cs index ade1fe7b4..3f9073849 100644 --- a/cs/Markdown/Tokens/NewLineToken.cs +++ b/cs/Markdown/Tokens/NewLineToken.cs @@ -3,5 +3,4 @@ namespace Markdown.Tokens; public class NewLineToken : IToken { public string Value => "\n"; - public int Length => 1; } \ No newline at end of file diff --git a/cs/Markdown/Tokens/TagToken.cs b/cs/Markdown/Tokens/TagToken.cs index da60426ca..5e62e2be6 100644 --- a/cs/Markdown/Tokens/TagToken.cs +++ b/cs/Markdown/Tokens/TagToken.cs @@ -2,12 +2,11 @@ namespace Markdown.Tokens; -public class TagToken(ITag tag, char left, char right) : IToken +public class TagToken(ITag tag, ContextString contextString) : IToken { public ITag Tag => tag; public TagStatus Status { get; set; } // TODO: we should get value of tag by his status? public string Value => throw new NotImplementedException(); - public int Length => Tag.MdTag.Length; - public (char left, char right) ContextChars => (left, right); + public ContextString Context => contextString; } \ No newline at end of file diff --git a/cs/Markdown/Tokens/TextToken.cs b/cs/Markdown/Tokens/TextToken.cs index 23c7c9515..18466f4c9 100644 --- a/cs/Markdown/Tokens/TextToken.cs +++ b/cs/Markdown/Tokens/TextToken.cs @@ -3,5 +3,4 @@ namespace Markdown.Tokens; public class TextToken(string value) : IToken { public string Value => value; - public int Length => Value.Length; } \ No newline at end of file From e91b0e29ebce26eae0a310dc914a91ee21cdfa4e Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 25 Nov 2024 13:24:43 +0500 Subject: [PATCH 07/13] Rename of Tokenizer to MdTokenizer --- cs/Markdown/Md.cs | 2 +- cs/Markdown/{Tokenizer.cs => MdTokenizer.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename cs/Markdown/{Tokenizer.cs => MdTokenizer.cs} (76%) diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index f80f6f152..826d10d3c 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -7,7 +7,7 @@ public static class Md private static readonly IEnumerable Tags = [new BoldTag(), new HeaderTag(), new ItalicTag()]; public static string Render(string markdownText) { - var tokens = new Tokenizer(Tags).Tokenize(markdownText); + var tokens = new MdTokenizer(Tags).Tokenize(markdownText); return new HtmlMdConverter().Convert(tokens); } } \ No newline at end of file diff --git a/cs/Markdown/Tokenizer.cs b/cs/Markdown/MdTokenizer.cs similarity index 76% rename from cs/Markdown/Tokenizer.cs rename to cs/Markdown/MdTokenizer.cs index ff86f5142..b7d486a66 100644 --- a/cs/Markdown/Tokenizer.cs +++ b/cs/Markdown/MdTokenizer.cs @@ -3,7 +3,7 @@ namespace Markdown; -public class Tokenizer(IEnumerable tags) +public class MdTokenizer(IEnumerable tags) { public List Tokenize(string markdownText) => throw new NotImplementedException(); } \ No newline at end of file From 89b922ff10ee11e03aa0b85d960d22e708b7e8e8 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 1 Dec 2024 15:42:17 +0500 Subject: [PATCH 08/13] new architecture 2.0 --- cs/Markdown/HtmlMdConverter.cs | 3 +- cs/Markdown/IMdConverter.cs | 4 +- cs/Markdown/Md.cs | 6 +-- cs/Markdown/MdTokenizer.cs | 5 +-- cs/Markdown/Models/Tag.cs | 5 +++ cs/Markdown/StringExtension.cs | 37 +++++++++++++++++++ cs/Markdown/Tags/BoldTag.cs | 8 ---- cs/Markdown/Tags/ContextString.cs | 3 -- cs/Markdown/Tags/EscapeMdTagKind.cs | 36 ++++++++++++++++++ cs/Markdown/Tags/HeaderTag.cs | 7 ---- cs/Markdown/Tags/IMdTagKind.cs | 16 ++++++++ cs/Markdown/Tags/ITag.cs | 13 ------- cs/Markdown/Tags/ItalicTag.cs | 9 ----- cs/Markdown/Tags/PairMdTagKind.cs | 57 +++++++++++++++++++++++++++++ cs/Markdown/Tags/PairTag.cs | 13 ------- cs/Markdown/Tags/SingleMdTagKind.cs | 48 ++++++++++++++++++++++++ cs/Markdown/Tags/SingleTag.cs | 13 ------- cs/Markdown/Tags/TagStatus.cs | 7 ---- cs/Markdown/Token.cs | 33 +++++++++++++++++ cs/Markdown/Tokens/EscapeToken.cs | 6 --- cs/Markdown/Tokens/IToken.cs | 6 --- cs/Markdown/Tokens/NewLineToken.cs | 6 --- cs/Markdown/Tokens/TagToken.cs | 12 ------ cs/Markdown/Tokens/TextToken.cs | 6 --- 24 files changed, 239 insertions(+), 120 deletions(-) create mode 100644 cs/Markdown/Models/Tag.cs create mode 100644 cs/Markdown/StringExtension.cs delete mode 100644 cs/Markdown/Tags/BoldTag.cs delete mode 100644 cs/Markdown/Tags/ContextString.cs create mode 100644 cs/Markdown/Tags/EscapeMdTagKind.cs delete mode 100644 cs/Markdown/Tags/HeaderTag.cs create mode 100644 cs/Markdown/Tags/IMdTagKind.cs delete mode 100644 cs/Markdown/Tags/ITag.cs delete mode 100644 cs/Markdown/Tags/ItalicTag.cs create mode 100644 cs/Markdown/Tags/PairMdTagKind.cs delete mode 100644 cs/Markdown/Tags/PairTag.cs create mode 100644 cs/Markdown/Tags/SingleMdTagKind.cs delete mode 100644 cs/Markdown/Tags/SingleTag.cs delete mode 100644 cs/Markdown/Tags/TagStatus.cs create mode 100644 cs/Markdown/Token.cs delete mode 100644 cs/Markdown/Tokens/EscapeToken.cs delete mode 100644 cs/Markdown/Tokens/IToken.cs delete mode 100644 cs/Markdown/Tokens/NewLineToken.cs delete mode 100644 cs/Markdown/Tokens/TagToken.cs delete mode 100644 cs/Markdown/Tokens/TextToken.cs diff --git a/cs/Markdown/HtmlMdConverter.cs b/cs/Markdown/HtmlMdConverter.cs index 2b785eb81..9730c5196 100644 --- a/cs/Markdown/HtmlMdConverter.cs +++ b/cs/Markdown/HtmlMdConverter.cs @@ -1,8 +1,7 @@ -using Markdown.Tokens; namespace Markdown; public class HtmlMdConverter : IMdConverter { - public string Convert(List tokens) => throw new NotImplementedException(); + public string Convert(Token root) => throw new NotImplementedException(); } \ No newline at end of file diff --git a/cs/Markdown/IMdConverter.cs b/cs/Markdown/IMdConverter.cs index bab454f53..4dc94921a 100644 --- a/cs/Markdown/IMdConverter.cs +++ b/cs/Markdown/IMdConverter.cs @@ -1,8 +1,6 @@ -using Markdown.Tokens; - namespace Markdown; public interface IMdConverter { - string Convert(List tokens); + string Convert(Token root); } \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 826d10d3c..a9d1f1384 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -4,10 +4,10 @@ namespace Markdown; public static class Md { - private static readonly IEnumerable Tags = [new BoldTag(), new HeaderTag(), new ItalicTag()]; + private static readonly IEnumerable Tags = []; public static string Render(string markdownText) { - var tokens = new MdTokenizer(Tags).Tokenize(markdownText); - return new HtmlMdConverter().Convert(tokens); + var root = new MdTokenizer(Tags).Tokenize(markdownText); + return new HtmlMdConverter().Convert(root); } } \ No newline at end of file diff --git a/cs/Markdown/MdTokenizer.cs b/cs/Markdown/MdTokenizer.cs index b7d486a66..61b866446 100644 --- a/cs/Markdown/MdTokenizer.cs +++ b/cs/Markdown/MdTokenizer.cs @@ -1,9 +1,8 @@ using Markdown.Tags; -using Markdown.Tokens; namespace Markdown; -public class MdTokenizer(IEnumerable tags) +public class MdTokenizer(IEnumerable tags) { - public List Tokenize(string markdownText) => throw new NotImplementedException(); + public Token Tokenize(string markdownText) => throw new NotImplementedException(); } \ No newline at end of file diff --git a/cs/Markdown/Models/Tag.cs b/cs/Markdown/Models/Tag.cs new file mode 100644 index 000000000..1763ff138 --- /dev/null +++ b/cs/Markdown/Models/Tag.cs @@ -0,0 +1,5 @@ +using Markdown.Tags; + +namespace Markdown.Models; + +public record Tag(int Position, IMdTagKind TagKind); \ No newline at end of file diff --git a/cs/Markdown/StringExtension.cs b/cs/Markdown/StringExtension.cs new file mode 100644 index 000000000..9009e6f17 --- /dev/null +++ b/cs/Markdown/StringExtension.cs @@ -0,0 +1,37 @@ +using Markdown.Tags; + +namespace Markdown; + +public static class StringExtension +{ + public static bool IsSubstring(this string text, int position, string value, bool isForward = true) + { + if (isForward ? position + value.Length > text.Length : position - value.Length < 0) return false; + + var substring = isForward + ? text.Substring(position, value.Length) + : text.Substring(position - value.Length, value.Length); + + return substring == value; + } + + public static bool? IsSubstring(this string text, int position, Predicate predicate, bool isForward = true) + { + if (isForward ? position + 1 > text.Length : position - 1 < 0) return null; + + position = isForward ? position : position - 1; + return predicate(text[position]); + } + + public static Token CreateToken(this string text, int startIndex, int stopIndex, IMdTagKind tag) + { + var value = text.Substring(startIndex, stopIndex - startIndex); + return new Token(value, startIndex, tag); + } + + public static int GetEndOfLinePosition(this string text, int startIndex = 0) + { + var newLinePosition = text.IndexOf(Environment.NewLine, startIndex, StringComparison.Ordinal); + return newLinePosition != -1 ? newLinePosition + Environment.NewLine.Length : text.Length; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tags/BoldTag.cs b/cs/Markdown/Tags/BoldTag.cs deleted file mode 100644 index be4cd2270..000000000 --- a/cs/Markdown/Tags/BoldTag.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Markdown.Tags; - -public class BoldTag : PairTag -{ - public override string MdOpenTag => "__"; - public override string MdCloseTag => MdOpenTag; - public override string HtmlTag => "strong"; -} \ No newline at end of file diff --git a/cs/Markdown/Tags/ContextString.cs b/cs/Markdown/Tags/ContextString.cs deleted file mode 100644 index bf9bf565b..000000000 --- a/cs/Markdown/Tags/ContextString.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Markdown.Tags; - -public record ContextString(string Left, string Right); \ No newline at end of file diff --git a/cs/Markdown/Tags/EscapeMdTagKind.cs b/cs/Markdown/Tags/EscapeMdTagKind.cs new file mode 100644 index 000000000..6ddd12aab --- /dev/null +++ b/cs/Markdown/Tags/EscapeMdTagKind.cs @@ -0,0 +1,36 @@ +using Markdown.Models; + +namespace Markdown.Tags; + +public class EscapeMdTagKind : IMdTagKind +{ + public string MdTag => "\\"; + public string HtmlOpenTag => string.Empty; + public string HtmlCloseTag => string.Empty; + + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) => text.IsSubstring(startIndex, MdTag); + + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag) + { + var openTagIndex = closeTags.IndexOf(openTag); + var escapedTag = openTagIndex + 1 > closeTags.Count + ? null + : closeTags[openTagIndex + 1]; + + if (escapedTag != null) + { + closeTag = escapedTag; + token = text.CreateToken(openTag.Position, + escapedTag.Position + escapedTag.TagKind.Length, this); + return true; + } + + closeTag = null!; + token = null!; + return false; + } + + public string RemoveMdTags(string text) => text.Remove(0, MdTag.Length); + + public string InsertHtmlTags(string text) => text; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/HeaderTag.cs b/cs/Markdown/Tags/HeaderTag.cs deleted file mode 100644 index 868c80c8b..000000000 --- a/cs/Markdown/Tags/HeaderTag.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Markdown.Tags; - -public class HeaderTag : SingleTag -{ - public override string MdOpenTag => "# "; - public override string HtmlTag => "h1"; -} \ No newline at end of file diff --git a/cs/Markdown/Tags/IMdTagKind.cs b/cs/Markdown/Tags/IMdTagKind.cs new file mode 100644 index 000000000..4921786cf --- /dev/null +++ b/cs/Markdown/Tags/IMdTagKind.cs @@ -0,0 +1,16 @@ +using Markdown.Models; + +namespace Markdown.Tags; + +public interface IMdTagKind +{ + public string MdTag { get; } + public string HtmlOpenTag { get; } + public string HtmlCloseTag { get; } + public int Length => MdTag.Length; + + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex); + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag); + public string RemoveMdTags(string text); + public string InsertHtmlTags(string text); +} \ No newline at end of file diff --git a/cs/Markdown/Tags/ITag.cs b/cs/Markdown/Tags/ITag.cs deleted file mode 100644 index 0a2b60a00..000000000 --- a/cs/Markdown/Tags/ITag.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Markdown.Tags; - -public interface ITag -{ - string MdOpenTag { get; } - string? MdCloseTag { get; } - string HtmlTag { get; } - bool SelfClosingTag { get; } - IEnumerable ForbiddenInside { get; } - - bool IsOpenedCorrectly(ContextString ctx); - bool IsClosedCorrectly(ContextString ctx); -} \ No newline at end of file diff --git a/cs/Markdown/Tags/ItalicTag.cs b/cs/Markdown/Tags/ItalicTag.cs deleted file mode 100644 index 5a1aa633e..000000000 --- a/cs/Markdown/Tags/ItalicTag.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Markdown.Tags; - -public class ItalicTag : PairTag -{ - public override string MdOpenTag => "_"; - public override string MdCloseTag => MdOpenTag; - public override string HtmlTag => "em"; - public override IEnumerable ForbiddenInside => [new BoldTag()]; -} \ No newline at end of file diff --git a/cs/Markdown/Tags/PairMdTagKind.cs b/cs/Markdown/Tags/PairMdTagKind.cs new file mode 100644 index 000000000..82cbd1a83 --- /dev/null +++ b/cs/Markdown/Tags/PairMdTagKind.cs @@ -0,0 +1,57 @@ +using Markdown.Models; + +namespace Markdown.Tags; + +public class PairMdTagKind(string mdTag, string htmlOpenTag, string htmlCloseTag) : IMdTagKind +{ + public string MdTag => mdTag; + public string HtmlOpenTag => htmlOpenTag; + public string HtmlCloseTag => htmlCloseTag; + + private bool IsValidTag(string text, int position) => + text.IsSubstring(position, MdTag) + && text.IsSubstring(position, char.IsDigit, false) != true + && text.IsSubstring(position + MdTag.Length, char.IsDigit, false) != true; + + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) + { + if(!IsValidTag(text, startIndex) || !IsValidTag(text, stopIndex - MdTag.Length)) return false; + + var value = text.Substring(startIndex, stopIndex - startIndex); + if (value.Split(' ').Length == 1) return value.Length > MdTag.Length * 2; + + return value.Split(Environment.NewLine).Length == 1 + && text.IsSubstring(startIndex, char.IsWhiteSpace, false) != false + && text.IsSubstring(stopIndex, char.IsWhiteSpace) != false; + } + + + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag) + { + foreach (var tag in closeTags.Where(tag => openTag != tag && + openTag.TagKind == tag.TagKind && + openTag.Position <= tag.Position && + openTag.TagKind.TokenCanBeCreated(text, openTag.Position, + tag.Position + tag.TagKind.Length))) + { + closeTag = tag; + token = text.CreateToken(openTag.Position, + tag.Position + tag.TagKind.Length, openTag.TagKind); + return true; + } + + closeTag = null!; + token = null!; + return false; + } + + public string RemoveMdTags(string text) => + text + .Remove(0, MdTag.Length) + .Remove(text.Length - MdTag.Length); + + public string InsertHtmlTags(string text) => + text + .Insert(0, HtmlOpenTag) + .Insert(text.Length, HtmlCloseTag); +} diff --git a/cs/Markdown/Tags/PairTag.cs b/cs/Markdown/Tags/PairTag.cs deleted file mode 100644 index 3918cb506..000000000 --- a/cs/Markdown/Tags/PairTag.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Markdown.Tags; - -public abstract class PairTag : ITag -{ - public abstract string MdOpenTag { get; } - public abstract string MdCloseTag { get; } - public abstract string HtmlTag { get; } - public bool SelfClosingTag => false; - public virtual IEnumerable ForbiddenInside => []; - - public virtual bool IsOpenedCorrectly(ContextString ctx) => ctx.Left.First() != ' '; - public virtual bool IsClosedCorrectly(ContextString ctx) => ctx.Right.First() != ' '; -} \ No newline at end of file diff --git a/cs/Markdown/Tags/SingleMdTagKind.cs b/cs/Markdown/Tags/SingleMdTagKind.cs new file mode 100644 index 000000000..c3863787c --- /dev/null +++ b/cs/Markdown/Tags/SingleMdTagKind.cs @@ -0,0 +1,48 @@ +using Markdown.Models; + +namespace Markdown.Tags; + +public class SingleMdTagKind(string mdTagKind, string htmlOpenTag, string htmlCloseTag) : IMdTagKind +{ + public string MdTag => mdTagKind; + public string HtmlOpenTag => htmlOpenTag; + public string HtmlCloseTag => htmlCloseTag; + + public SingleMdTagKind() : this(string.Empty, string.Empty, string.Empty) + { + } + + private bool IsValidTag(string text, int position) => + text.IsSubstring(position, MdTag) + && (text.IsSubstring(position, Environment.NewLine, false) || position == 0); + + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) => + IsValidTag(text, startIndex) + && (text.IsSubstring(stopIndex, Environment.NewLine, false) || text.Length == stopIndex); + + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag) + { + var closeTagIndex = text.GetEndOfLinePosition(openTag.Position); + + if (TokenCanBeCreated(text, openTag.Position, closeTagIndex)) + { + closeTag = null!; + token = text.CreateToken(openTag.Position, closeTagIndex, this); + return true; + } + + closeTag = null!; + token = null!; + return false; + + } + + public string RemoveMdTags(string text) => text.Remove(0, MdTag.Length); + + public string InsertHtmlTags(string text) => + text + .Insert(0, HtmlOpenTag) + .Insert(text.EndsWith(Environment.NewLine) + ? text.Length - Environment.NewLine.Length + : text.Length, HtmlCloseTag); +} \ No newline at end of file diff --git a/cs/Markdown/Tags/SingleTag.cs b/cs/Markdown/Tags/SingleTag.cs deleted file mode 100644 index 428bb6048..000000000 --- a/cs/Markdown/Tags/SingleTag.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Markdown.Tags; - -public abstract class SingleTag : ITag -{ - public abstract string MdOpenTag { get; } - public string? MdCloseTag => null; - public abstract string HtmlTag { get; } - public bool SelfClosingTag => true; - public virtual IEnumerable ForbiddenInside => []; - - public virtual bool IsOpenedCorrectly(ContextString ctx) => ctx.Left.Contains('\n'); - public bool IsClosedCorrectly(ContextString ctx) => true; -} \ No newline at end of file diff --git a/cs/Markdown/Tags/TagStatus.cs b/cs/Markdown/Tags/TagStatus.cs deleted file mode 100644 index ddfa035d0..000000000 --- a/cs/Markdown/Tags/TagStatus.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Markdown.Tags; - -public enum TagStatus -{ - Opened, - Closed -} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs new file mode 100644 index 000000000..605f73907 --- /dev/null +++ b/cs/Markdown/Token.cs @@ -0,0 +1,33 @@ +using Markdown.Tags; + +namespace Markdown; + +public class Token(string value, int position, IMdTagKind mdTagKind) +{ + private readonly List children = []; + + public string Value => value; + public IMdTagKind Tag => mdTagKind; + public int Position { get; private set; } = position; + + public Token(string value) : this(value, value.Length, new SingleMdTagKind()) + { + } + + public void AddToken(Token child) + { + var parent = children.FirstOrDefault(t => t.IsChild(child)); + + if (parent == null) children.Add(child); + else + { + parent.AddToken(child); + child.Position -= parent.Position + parent.Tag.MdTag.Length; + } + } + + public bool IsChild(Token child) => + child.Position >= Position && + child.Position < Position + Value.Length && + child.Position + child.Value.Length <= Position + Value.Length; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/EscapeToken.cs b/cs/Markdown/Tokens/EscapeToken.cs deleted file mode 100644 index f7e7e63c9..000000000 --- a/cs/Markdown/Tokens/EscapeToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Markdown.Tokens; - -public class EscapeToken : IToken -{ - public string Value => "\\"; -} \ No newline at end of file diff --git a/cs/Markdown/Tokens/IToken.cs b/cs/Markdown/Tokens/IToken.cs deleted file mode 100644 index e72de6a68..000000000 --- a/cs/Markdown/Tokens/IToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Markdown.Tokens; - -public interface IToken -{ - string Value { get; } -} \ No newline at end of file diff --git a/cs/Markdown/Tokens/NewLineToken.cs b/cs/Markdown/Tokens/NewLineToken.cs deleted file mode 100644 index 3f9073849..000000000 --- a/cs/Markdown/Tokens/NewLineToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Markdown.Tokens; - -public class NewLineToken : IToken -{ - public string Value => "\n"; -} \ No newline at end of file diff --git a/cs/Markdown/Tokens/TagToken.cs b/cs/Markdown/Tokens/TagToken.cs deleted file mode 100644 index 5e62e2be6..000000000 --- a/cs/Markdown/Tokens/TagToken.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Markdown.Tags; - -namespace Markdown.Tokens; - -public class TagToken(ITag tag, ContextString contextString) : IToken -{ - public ITag Tag => tag; - public TagStatus Status { get; set; } - // TODO: we should get value of tag by his status? - public string Value => throw new NotImplementedException(); - public ContextString Context => contextString; -} \ No newline at end of file diff --git a/cs/Markdown/Tokens/TextToken.cs b/cs/Markdown/Tokens/TextToken.cs deleted file mode 100644 index 18466f4c9..000000000 --- a/cs/Markdown/Tokens/TextToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Markdown.Tokens; - -public class TextToken(string value) : IToken -{ - public string Value => value; -} \ No newline at end of file From 711d9f0804fff0610355826342c09a15d0a8d297 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 1 Dec 2024 18:59:03 +0500 Subject: [PATCH 09/13] Added base implementation of Md Render with basic Tests --- cs/Markdown/HtmlMdConverter.cs | 7 -- cs/Markdown/IMdConverter.cs | 6 -- cs/Markdown/Md.cs | 46 +++++++++++- cs/Markdown/MdTokenizer.cs | 107 +++++++++++++++++++++++++++- cs/Markdown/StringExtension.cs | 7 ++ cs/Markdown/Tags/EscapeMdTagKind.cs | 16 +++-- cs/Markdown/Tags/IMdTagKind.cs | 2 +- cs/Markdown/Tags/PairMdTagKind.cs | 39 +++++----- cs/Markdown/Tags/SingleMdTagKind.cs | 15 ++-- cs/Markdown/Token.cs | 29 +++++--- cs/MarkdownTests/MdTests.cs | 76 ++++++++++++++++++++ 11 files changed, 288 insertions(+), 62 deletions(-) delete mode 100644 cs/Markdown/HtmlMdConverter.cs delete mode 100644 cs/Markdown/IMdConverter.cs create mode 100644 cs/MarkdownTests/MdTests.cs diff --git a/cs/Markdown/HtmlMdConverter.cs b/cs/Markdown/HtmlMdConverter.cs deleted file mode 100644 index 9730c5196..000000000 --- a/cs/Markdown/HtmlMdConverter.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace Markdown; - -public class HtmlMdConverter : IMdConverter -{ - public string Convert(Token root) => throw new NotImplementedException(); -} \ No newline at end of file diff --git a/cs/Markdown/IMdConverter.cs b/cs/Markdown/IMdConverter.cs deleted file mode 100644 index 4dc94921a..000000000 --- a/cs/Markdown/IMdConverter.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Markdown; - -public interface IMdConverter -{ - string Convert(Token root); -} \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index a9d1f1384..d58cf0ad4 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -4,10 +4,50 @@ namespace Markdown; public static class Md { - private static readonly IEnumerable Tags = []; + private static IEnumerable Tags + { + get + { + yield return new EscapeMdTagKind(); + yield return new SingleMdTagKind("#", "

", "

"); + yield return new PairMdTagKind("_", "", ""); + yield return new PairMdTagKind("__", "", ""); + } + } + + private static IEnumerable, bool>> TagRules + { + get + { + yield return IgnoreIntersectionBetweenPairTagsRule; + yield return IgnorePairTagWhenParentPairTagHasGreaterLengthRule; + } + } + public static string Render(string markdownText) { - var root = new MdTokenizer(Tags).Tokenize(markdownText); - return new HtmlMdConverter().Convert(root); + var root = new MdTokenizer(Tags.ToList(), TagRules).Tokenize(markdownText); + return root.ConvertToHtml(); } + + private static bool IgnorePairTagWhenParentPairTagHasGreaterLengthRule(Token tokenToCheck, + IEnumerable tokens) => + tokenToCheck.Tag is PairMdTagKind + && tokens + .Where(t => t != tokenToCheck && t.Tag is PairMdTagKind) + .Any(parent => parent.IsChild(tokenToCheck) + && !(parent.Tag.MdTag.Length > tokenToCheck.Tag.MdTag.Length)); + + + private static bool IgnoreIntersectionBetweenPairTagsRule(Token tokenToCheck, IEnumerable tokens) => + tokenToCheck.Tag is PairMdTagKind + && tokens + .Where(t => t != tokenToCheck && t.Tag is PairMdTagKind) + .Any(t => IsIntersectionBetween(tokenToCheck, t) + || IsIntersectionBetween(t, tokenToCheck)); + + private static bool IsIntersectionBetween(Token token, Token otherToken) => + token.Position > otherToken.Position + && token.Position < otherToken.Position + otherToken.Value.Length + && token.Position + token.Value.Length > otherToken.Position + otherToken.Value.Length; } \ No newline at end of file diff --git a/cs/Markdown/MdTokenizer.cs b/cs/Markdown/MdTokenizer.cs index 61b866446..76b6aacc5 100644 --- a/cs/Markdown/MdTokenizer.cs +++ b/cs/Markdown/MdTokenizer.cs @@ -1,8 +1,111 @@ +using Markdown.Models; using Markdown.Tags; namespace Markdown; -public class MdTokenizer(IEnumerable tags) +public class MdTokenizer(List tags, IEnumerable, bool>> tagRules) { - public Token Tokenize(string markdownText) => throw new NotImplementedException(); + private readonly Dictionary availableTags = tags.ToDictionary(tag => tag.MdTag, tag => tag); + private readonly List, bool>> tagRules = tagRules.ToList(); + private readonly List mdLenOfTagSignatures = tags + .Select(tag => tag.MdTag.Length) + .Distinct() + .OrderDescending() + .ToList(); + + public Token Tokenize(string text) + { + var root = new Token(text); + + foreach (var line in GetLines(text)) + { + var tokens = GetTokens(line.Value).OrderBy(t => t.Position).ToList(); + foreach (var token in tokens + .Where(t => tagRules.Select(rule => rule(t, tokens)) + .All(result => !result))) line.AddToken(token); + root.AddToken(line); + } + + return root; + } + + private static IEnumerable GetLines(string text) + { + var position = 0; + foreach (var line in text.Split(Environment.NewLine)) + { + yield return new Token(line, position, new SingleMdTagKind()); + position += line.Length + Environment.NewLine.Length; + } + } + + private IEnumerable GetTokens(string text) + { + var tags = GetTags(text).ToList(); + var escapeTokens = ParseEscapedTokens(text, tags).ToList(); + + return ParseTokens(text, tags).Concat(escapeTokens); + } + + private IEnumerable GetTags(string text) + { + for (var pos = 0; pos < text.Length; pos++) + { + if (!TryGetTag(text, pos, out var tag)) continue; + + yield return new Tag(pos, tag); + + pos += tag.Length - 1; + } + } + + private bool TryGetTag(string text, int position, out IMdTagKind mdTag) + { + foreach (var mdLenOfTagSignature in mdLenOfTagSignatures) + { + if (position + mdLenOfTagSignature > text.Length || !availableTags + .TryGetValue(text.Substring(position, mdLenOfTagSignature), out var tag)) continue; + + mdTag = tag; + return true; + } + + mdTag = null!; + return false; + } + + private static IEnumerable ParseEscapedTokens(string text, List tags) + { + for (var idx = 0; idx < tags.Count - 1; idx += 1) + { + if (tags[idx].TagKind is not EscapeMdTagKind) continue; + + var position = tags[idx].Position; + tags.Remove(tags[idx]); + + if (tags[idx].Position - position == 1) + { + yield return text.CreateEscapeToken(tags[idx]); + tags.Remove(tags[idx]); + } + + idx -= 1; + } + } + + private static IEnumerable ParseTokens(string text, List tags) + { + for (var idx = 0; idx < tags.Count; idx += 1) + { + if (!tags[idx].TagKind.TryGetToken(text, tags[idx], tags, out var token, + out var closeToken)) continue; + + if (closeToken != null) tags.Remove(closeToken); + + yield return token; + tags.RemoveAt(idx); + + idx -= 1; + } + } } \ No newline at end of file diff --git a/cs/Markdown/StringExtension.cs b/cs/Markdown/StringExtension.cs index 9009e6f17..6ad4f2a6b 100644 --- a/cs/Markdown/StringExtension.cs +++ b/cs/Markdown/StringExtension.cs @@ -1,3 +1,4 @@ +using Markdown.Models; using Markdown.Tags; namespace Markdown; @@ -34,4 +35,10 @@ public static int GetEndOfLinePosition(this string text, int startIndex = 0) var newLinePosition = text.IndexOf(Environment.NewLine, startIndex, StringComparison.Ordinal); return newLinePosition != -1 ? newLinePosition + Environment.NewLine.Length : text.Length; } + + public static Token CreateEscapeToken(this string text, Tag escapeTag) + { + var value = text.Substring(escapeTag.Position - 1, escapeTag.TagKind.Length); + return new Token(value, escapeTag.Position - 1, new EscapeMdTagKind()); + } } \ No newline at end of file diff --git a/cs/Markdown/Tags/EscapeMdTagKind.cs b/cs/Markdown/Tags/EscapeMdTagKind.cs index 6ddd12aab..9631c7b09 100644 --- a/cs/Markdown/Tags/EscapeMdTagKind.cs +++ b/cs/Markdown/Tags/EscapeMdTagKind.cs @@ -8,23 +8,25 @@ public class EscapeMdTagKind : IMdTagKind public string HtmlOpenTag => string.Empty; public string HtmlCloseTag => string.Empty; - public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) => text.IsSubstring(startIndex, MdTag); + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) => + text.IsSubstring(startIndex, MdTag); - public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag) + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, + out Tag closeTag) { - var openTagIndex = closeTags.IndexOf(openTag); + var openTagIndex = closeTags.IndexOf(openTag); var escapedTag = openTagIndex + 1 > closeTags.Count ? null : closeTags[openTagIndex + 1]; - + if (escapedTag != null) { closeTag = escapedTag; - token = text.CreateToken(openTag.Position, - escapedTag.Position + escapedTag.TagKind.Length, this); + token = text.CreateToken(openTag.Position, escapedTag.Position + + escapedTag.TagKind.Length, this); return true; } - + closeTag = null!; token = null!; return false; diff --git a/cs/Markdown/Tags/IMdTagKind.cs b/cs/Markdown/Tags/IMdTagKind.cs index 4921786cf..03c872800 100644 --- a/cs/Markdown/Tags/IMdTagKind.cs +++ b/cs/Markdown/Tags/IMdTagKind.cs @@ -10,7 +10,7 @@ public interface IMdTagKind public int Length => MdTag.Length; public bool TokenCanBeCreated(string text, int startIndex, int stopIndex); - public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag); + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag? closeTag); public string RemoveMdTags(string text); public string InsertHtmlTags(string text); } \ No newline at end of file diff --git a/cs/Markdown/Tags/PairMdTagKind.cs b/cs/Markdown/Tags/PairMdTagKind.cs index 82cbd1a83..581597ffa 100644 --- a/cs/Markdown/Tags/PairMdTagKind.cs +++ b/cs/Markdown/Tags/PairMdTagKind.cs @@ -7,32 +7,31 @@ public class PairMdTagKind(string mdTag, string htmlOpenTag, string htmlCloseTag public string MdTag => mdTag; public string HtmlOpenTag => htmlOpenTag; public string HtmlCloseTag => htmlCloseTag; - - private bool IsValidTag(string text, int position) => - text.IsSubstring(position, MdTag) - && text.IsSubstring(position, char.IsDigit, false) != true - && text.IsSubstring(position + MdTag.Length, char.IsDigit, false) != true; + private bool IsValidTag(string text, int position) => + text.IsSubstring(position, MdTag) + && text.IsSubstring(position, char.IsDigit, false) != true + && text.IsSubstring(position + MdTag.Length, char.IsDigit) != true; + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) { - if(!IsValidTag(text, startIndex) || !IsValidTag(text, stopIndex - MdTag.Length)) return false; - + if (!IsValidTag(text, startIndex) || !IsValidTag(text, stopIndex - MdTag.Length)) return false; + var value = text.Substring(startIndex, stopIndex - startIndex); if (value.Split(' ').Length == 1) return value.Length > MdTag.Length * 2; - + return value.Split(Environment.NewLine).Length == 1 && text.IsSubstring(startIndex, char.IsWhiteSpace, false) != false && text.IsSubstring(stopIndex, char.IsWhiteSpace) != false; } - public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag) { - foreach (var tag in closeTags.Where(tag => openTag != tag && - openTag.TagKind == tag.TagKind && - openTag.Position <= tag.Position && - openTag.TagKind.TokenCanBeCreated(text, openTag.Position, - tag.Position + tag.TagKind.Length))) + foreach (var tag in closeTags.Where(t => openTag != t + && openTag.TagKind == t.TagKind + && openTag.Position <= t.Position + && openTag.TagKind.TokenCanBeCreated(text, openTag.Position, + t.Position + t.TagKind.Length))) { closeTag = tag; token = text.CreateToken(openTag.Position, @@ -47,11 +46,11 @@ public bool TryGetToken(string text, Tag openTag, List closeTags, out Token public string RemoveMdTags(string text) => text - .Remove(0, MdTag.Length) - .Remove(text.Length - MdTag.Length); - - public string InsertHtmlTags(string text) => + .Remove(text.Length - MdTag.Length) + .Remove(0, MdTag.Length); + + public string InsertHtmlTags(string text) => text - .Insert(0, HtmlOpenTag) - .Insert(text.Length, HtmlCloseTag); + .Insert(text.Length, HtmlCloseTag) + .Insert(0, HtmlOpenTag); } diff --git a/cs/Markdown/Tags/SingleMdTagKind.cs b/cs/Markdown/Tags/SingleMdTagKind.cs index c3863787c..3c5e5b8f9 100644 --- a/cs/Markdown/Tags/SingleMdTagKind.cs +++ b/cs/Markdown/Tags/SingleMdTagKind.cs @@ -13,17 +13,17 @@ public SingleMdTagKind() : this(string.Empty, string.Empty, string.Empty) } private bool IsValidTag(string text, int position) => - text.IsSubstring(position, MdTag) + text.IsSubstring(position, MdTag) && (text.IsSubstring(position, Environment.NewLine, false) || position == 0); - + public bool TokenCanBeCreated(string text, int startIndex, int stopIndex) => IsValidTag(text, startIndex) && (text.IsSubstring(stopIndex, Environment.NewLine, false) || text.Length == stopIndex); - public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, out Tag closeTag) + public bool TryGetToken(string text, Tag openTag, List closeTags, out Token token, + out Tag? closeTag) { var closeTagIndex = text.GetEndOfLinePosition(openTag.Position); - if (TokenCanBeCreated(text, openTag.Position, closeTagIndex)) { closeTag = null!; @@ -31,18 +31,17 @@ public bool TryGetToken(string text, Tag openTag, List closeTags, out Token return true; } - closeTag = null!; + closeTag = null; token = null!; return false; - } public string RemoveMdTags(string text) => text.Remove(0, MdTag.Length); public string InsertHtmlTags(string text) => text - .Insert(0, HtmlOpenTag) .Insert(text.EndsWith(Environment.NewLine) ? text.Length - Environment.NewLine.Length - : text.Length, HtmlCloseTag); + : text.Length, HtmlCloseTag) + .Insert(0, HtmlOpenTag); } \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 605f73907..efc4a0b3b 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -1,3 +1,4 @@ +using System.Text; using Markdown.Tags; namespace Markdown; @@ -13,21 +14,33 @@ public class Token(string value, int position, IMdTagKind mdTagKind) public Token(string value) : this(value, value.Length, new SingleMdTagKind()) { } - + public void AddToken(Token child) { - var parent = children.FirstOrDefault(t => t.IsChild(child)); + var parent = children.FirstOrDefault(token => token.IsChild(child)); - if (parent == null) children.Add(child); - else + if (parent != null) { parent.AddToken(child); child.Position -= parent.Position + parent.Tag.MdTag.Length; } + else children.Add(child); + } + + public string ConvertToHtml() + { + var sb = new StringBuilder(Tag.RemoveMdTags(Value)); + foreach (var child in children.OrderByDescending(token => token.Position)) + { + sb.Remove(child.Position, child.Value.Length); + sb.Insert(child.Position, child.ConvertToHtml()); + } + + return Tag.InsertHtmlTags(sb.ToString()); } - public bool IsChild(Token child) => - child.Position >= Position && - child.Position < Position + Value.Length && - child.Position + child.Value.Length <= Position + Value.Length; + public bool IsChild(Token child) => + child.Position >= Position + && child.Position < Position + Value.Length + && child.Position + child.Value.Length <= Position + Value.Length; } \ No newline at end of file diff --git a/cs/MarkdownTests/MdTests.cs b/cs/MarkdownTests/MdTests.cs new file mode 100644 index 000000000..195b570a2 --- /dev/null +++ b/cs/MarkdownTests/MdTests.cs @@ -0,0 +1,76 @@ +using Markdown; +using FluentAssertions; +namespace MarkdownTests; + +[TestFixture] +[TestOf(typeof(Md))] +public class MdTests +{ + [TestCaseSource(nameof(ConvertTagsTests))] + [TestCaseSource(nameof(MdSpecTests))] + public void Render_ShouldWorkCorrectly(string input, string expected) => Md.Render(input).Should().Be(expected); + + public static IEnumerable ConvertTagsTests + { + get + { + yield return new TestCaseData( + $"# Заголовок{Environment.NewLine}#Заголовок", $"

Заголовок

{Environment.NewLine}

Заголовок

") + .SetName("Render_ShouldConvertHeaderTag") + .SetCategory(nameof(ConvertTagsTests)); + yield return new TestCaseData( + $"_курсивный текст_{Environment.NewLine}_курсивный текст_", + $"курсивный текст{Environment.NewLine}курсивный текст") + .SetName("Render_ShouldConvertItalicTag") + .SetCategory(nameof(ConvertTagsTests)); + yield return new TestCaseData( + $"__полужирный текст__{Environment.NewLine}__полужирный текст__", + $"полужирный текст{Environment.NewLine}полужирный текст") + .SetName("Render_ShouldConvertBoldTag") + .SetCategory(nameof(ConvertTagsTests)); + yield return new TestCaseData( + "_чем\\_ 100_ __раз_ услышать.__", + "_чем_ 100_ __раз_ услышать.__") + .SetName("Render_ShouldConvertEscapeTag") + .SetCategory(nameof(ConvertTagsTests)); + yield return new TestCaseData( + "# Заголовок c _курсивным текстом_ и __полужирным текстом__", + "

Заголовок c курсивным текстом и полужирным текстом

") + .SetName("Render_ShouldConvertAllTagsInHeader") + .SetCategory(nameof(ConvertTagsTests)); + } + } + + public static IEnumerable MdSpecTests + { + get + { + yield return new TestCaseData( + $"#Это заголовок, а это (#) - нет.{Environment.NewLine}И это #тоже# не ##заголовок##", + $"

Это заголовок, а это (#) - нет.

{Environment.NewLine}И это #тоже# не ##заголовок##") + .SetName("Render_ShouldIgnoreSingleTags_WhenNotStartsWithNewLine") + .SetCategory("BasicSpec"); + yield return new TestCaseData( + $"Это _заголовок1_ ,а не заголовок __1 уровня__{Environment.NewLine}_4 Life CJ, _Grove __123__ Street_ 4 Life_", + $"Это _заголовок1_ ,а не заголовок __1 уровня__{Environment.NewLine}_4 Life CJ, Grove __123__ Street 4 Life_") + .SetName("Render_ShouldIgnorePairTags_WhenPlacedWithNumbers") + .SetCategory("MdSpec"); + yield return new TestCaseData( + $"Подчерки могут выделять часть слова{Environment.NewLine}Но в раз_ных сло_вах н__е мог__ут", + $"Подчерки могут выделять часть слова{Environment.NewLine}Но в раз_ных сло_вах н__е мог__ут") + .SetName("Render_ShouldIgnorePairTags_WhenPartsOfDifferentWordsMarked") + .SetCategory("MdSpec"); + yield return new TestCaseData( + "В случае __пересечения _двойных__ и одинарных_ подчерков ни _один из __них не_ считается__ выделением", + "В случае __пересечения _двойных__ и одинарных_ подчерков ни _один из __них не_ считается__ выделением") + .SetName("Render_ShouldIgnorePairTags_WhenIntersection") + .SetCategory("MdSpec"); + yield return new TestCaseData( + "Если внутри подчерков пустая строка ____, то они остаются символами подчерка", + "Если внутри подчерков пустая строка ____, то они остаются символами подчерка") + .SetName("Render_ShouldIgnorePairTags_WhenTextIsEmpty") + .SetCategory("MdSpec"); + } + } +} + From 0bb79b7c441f3e4882cda390764f9f76146630ba Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 2 Dec 2024 00:10:42 +0500 Subject: [PATCH 10/13] Added test for Linear time complexity of Md Render --- cs/MarkdownTests/MdTests.cs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cs/MarkdownTests/MdTests.cs b/cs/MarkdownTests/MdTests.cs index 195b570a2..6a166e855 100644 --- a/cs/MarkdownTests/MdTests.cs +++ b/cs/MarkdownTests/MdTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Markdown; using FluentAssertions; namespace MarkdownTests; @@ -9,7 +10,36 @@ public class MdTests [TestCaseSource(nameof(ConvertTagsTests))] [TestCaseSource(nameof(MdSpecTests))] public void Render_ShouldWorkCorrectly(string input, string expected) => Md.Render(input).Should().Be(expected); - + + [TestCase(100, 10)] + [TestCase(10, 100)] + [TestCase(1, 1000)] + public void Render_ShouldWorkLinearly(int times, int inputScale) + { + const string input = "# Заголовок c _курсивным текстом_ и __полужирным текстом__"; + var scaledInput = string.Join(Environment.NewLine, Enumerable.Repeat(input, inputScale)); + var timeWithDefaultInput = MeasureRenderTime(input, times); + var timeWithScaledInput = MeasureRenderTime(scaledInput, times); + var avgWithDefaultInput = timeWithDefaultInput / times; + var avgWithScaledInput = timeWithScaledInput / (inputScale * times); + + avgWithDefaultInput.Should().NotBeCloseTo(avgWithScaledInput, TimeSpan.FromTicks(20)); + } + + + private static TimeSpan MeasureRenderTime(string input, int times = 1) + { + var timer = new Stopwatch(); + + for (var i = 0; i < times; i++) + { + timer.Start(); + Md.Render(input); + timer.Stop(); + } + + return timer.Elapsed; + } public static IEnumerable ConvertTagsTests { get From e764bda0a27827c39c4b7af93a6280f48bd68dec Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 2 Dec 2024 01:58:03 +0500 Subject: [PATCH 11/13] small fixes in one of the Md tests --- cs/MarkdownTests/MdTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cs/MarkdownTests/MdTests.cs b/cs/MarkdownTests/MdTests.cs index 6a166e855..e61a65c57 100644 --- a/cs/MarkdownTests/MdTests.cs +++ b/cs/MarkdownTests/MdTests.cs @@ -12,8 +12,8 @@ public class MdTests public void Render_ShouldWorkCorrectly(string input, string expected) => Md.Render(input).Should().Be(expected); [TestCase(100, 10)] - [TestCase(10, 100)] - [TestCase(1, 1000)] + [TestCase(100, 100)] + [TestCase(100, 1000)] public void Render_ShouldWorkLinearly(int times, int inputScale) { const string input = "# Заголовок c _курсивным текстом_ и __полужирным текстом__"; @@ -23,7 +23,7 @@ public void Render_ShouldWorkLinearly(int times, int inputScale) var avgWithDefaultInput = timeWithDefaultInput / times; var avgWithScaledInput = timeWithScaledInput / (inputScale * times); - avgWithDefaultInput.Should().NotBeCloseTo(avgWithScaledInput, TimeSpan.FromTicks(20)); + avgWithDefaultInput.Should().BeCloseTo(avgWithScaledInput, TimeSpan.FromTicks(3300)); } From f9c3b56f7f64650ec980e1c3ec0ad586ccb2c5e6 Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 2 Dec 2024 09:55:23 +0500 Subject: [PATCH 12/13] small refactor in MdTokenizer --- cs/Markdown/MdTokenizer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cs/Markdown/MdTokenizer.cs b/cs/Markdown/MdTokenizer.cs index 76b6aacc5..bdbcc7c16 100644 --- a/cs/Markdown/MdTokenizer.cs +++ b/cs/Markdown/MdTokenizer.cs @@ -49,13 +49,13 @@ private IEnumerable GetTokens(string text) private IEnumerable GetTags(string text) { - for (var pos = 0; pos < text.Length; pos++) + for (var position = 0; position < text.Length; position += 1) { - if (!TryGetTag(text, pos, out var tag)) continue; + if (!TryGetTag(text, position, out var tag)) continue; - yield return new Tag(pos, tag); + yield return new Tag(position, tag); - pos += tag.Length - 1; + position += tag.Length - 1; } } From 4b9415b9c6aed7211bd39819ac996032765496f1 Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 2 Dec 2024 11:50:59 +0500 Subject: [PATCH 13/13] Added some more tests for MdTests --- cs/MarkdownTests/MdTests.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cs/MarkdownTests/MdTests.cs b/cs/MarkdownTests/MdTests.cs index e61a65c57..34bfa22d8 100644 --- a/cs/MarkdownTests/MdTests.cs +++ b/cs/MarkdownTests/MdTests.cs @@ -85,15 +85,35 @@ public static IEnumerable MdSpecTests $"Это _заголовок1_ ,а не заголовок __1 уровня__{Environment.NewLine}_4 Life CJ, Grove __123__ Street 4 Life_") .SetName("Render_ShouldIgnorePairTags_WhenPlacedWithNumbers") .SetCategory("MdSpec"); + yield return new TestCaseData( + $"Подчерки _мо_гут вы__де__лять ча_сть_ слова{Environment.NewLine}Но в разных словах не могут", + $"Подчерки могут выделять часть слова{Environment.NewLine}Но в разных словах не могут") + .SetName("Render_ShouldConvertPairTags_WhenPartOfWordMarked") + .SetCategory("MdSpec"); yield return new TestCaseData( $"Подчерки могут выделять часть слова{Environment.NewLine}Но в раз_ных сло_вах н__е мог__ут", $"Подчерки могут выделять часть слова{Environment.NewLine}Но в раз_ных сло_вах н__е мог__ут") .SetName("Render_ShouldIgnorePairTags_WhenPartsOfDifferentWordsMarked") .SetCategory("MdSpec"); + yield return new TestCaseData( + $"За подчерками, _начинающими выделение,_ должен следовать __непробельный символ__{Environment.NewLine}Иначе_ ничего_ не__ получится!__", + $"За подчерками, начинающими выделение, должен следовать непробельный символ{Environment.NewLine}Иначе_ ничего_ не__ получится!__") + .SetName("Render_ShouldIgnorePairTags_WhenMarkedWordsStartsWithNonWhitespace") + .SetCategory("MdSpec"); + yield return new TestCaseData( + $"Подчерки, _заканчивающие выделение,_ должны следовать за __непробельным символом__{Environment.NewLine}_Иначе _ничего __не получится __!", + $"Подчерки, заканчивающие выделение, должны следовать за непробельным символом{Environment.NewLine}_Иначе _ничего __не получится __!") + .SetName("Render_ShouldIgnorePairTags_WhenMarkedWordsEndsWithNonWhitespace") + .SetCategory("MdSpec"); yield return new TestCaseData( "В случае __пересечения _двойных__ и одинарных_ подчерков ни _один из __них не_ считается__ выделением", "В случае __пересечения _двойных__ и одинарных_ подчерков ни _один из __них не_ считается__ выделением") - .SetName("Render_ShouldIgnorePairTags_WhenIntersection") + .SetName("Render_ShouldIgnorePairTags_WhenIntersectionBetweenDifferentKindsOfPairTags") + .SetCategory("MdSpec"); + yield return new TestCaseData( + $"__Непарные _символы в рамках{Environment.NewLine}одного_ абзаца не считаются__ выделением", + $"__Непарные _символы в рамках{Environment.NewLine}одного_ абзаца не считаются__ выделением") + .SetName("Render_ShouldIgnorePairTags_WhenPlacedInMultiLine") .SetCategory("MdSpec"); yield return new TestCaseData( "Если внутри подчерков пустая строка ____, то они остаются символами подчерка",