diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj
new file mode 100644
index 000000000..a6977591a
--- /dev/null
+++ b/cs/Markdown/Markdown.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ Markdown.Program
+
+
+
diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs
new file mode 100644
index 000000000..7a19ad69a
--- /dev/null
+++ b/cs/Markdown/Md.cs
@@ -0,0 +1,55 @@
+using Markdown.Tags;
+using System.Text;
+
+namespace Markdown
+{
+ public class Md
+ {
+ public static readonly IReadOnlyDictionary>> MdTags =
+ new Dictionary>>()
+ {
+ {
+ '_', new Dictionary>
+ {
+ { "_", (markdown, tagStart) => new Italic(markdown, tagStart)},
+ { "__", (markdown, tagStart) => new Bold(markdown, tagStart)}
+ }
+ },
+ {
+ '#', new Dictionary>
+ {
+ { "#", (markdown, tagStart) => new Header(markdown, tagStart) }
+ }
+ },
+ {
+ '\\',
+ new Dictionary>
+ {
+ { "\\", (markdown, tagStart) => new Escape(markdown, tagStart) }
+ }
+ }
+ };
+
+ public string Render(string markdownText)
+ {
+ var parser = new MdParser(markdownText, MdTags);
+ var tags = parser.GetTags();
+ var result = new StringBuilder();
+ var tagsStart = tags.ToDictionary(t => t.TagStart);
+ for (var i = 0; i < markdownText.Length;)
+ {
+ if (tagsStart.ContainsKey(i))
+ {
+ result.Append(tagsStart[i].RenderToHtml());
+ i = tagsStart[i].TagEnd++;
+ }
+ else
+ {
+ result.Append(markdownText[i]);
+ i++;
+ }
+ }
+ return result.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/MdParser.cs b/cs/Markdown/MdParser.cs
new file mode 100644
index 000000000..36c5f94ca
--- /dev/null
+++ b/cs/Markdown/MdParser.cs
@@ -0,0 +1,142 @@
+using Markdown.Tags;
+using Markdown.Tags.TagsInteraction;
+
+namespace Markdown;
+
+public class MdParser(string markdownText, IReadOnlyDictionary>> tagCreators)
+{
+ private class ReaderPosition
+ {
+ public int Position { get; set; }
+ }
+
+ public Tag[] GetTags()
+ {
+ var lines = markdownText.Split('\n');
+ var result = new List();
+ foreach (var line in lines)
+ {
+ var tokens = Tokenize(markdownText);
+ var possibleTags = FindCorrectTags(tokens);
+ var possibleTagsStarts = possibleTags.Select(t => t.TagStart).ToHashSet();
+ var current = new ReaderPosition();
+ var tagsIterator = possibleTags.OrderBy(t => t.TagStart).GetEnumerator();
+ while (tagsIterator.MoveNext())
+ {
+ var newTag = CreateTag(current, [], tagsIterator, possibleTagsStarts);
+ if (newTag.IsTagClosed)
+ result.Add(newTag);
+ }
+ }
+
+ return result.ToArray();
+ }
+
+ private Tag CreateTag(ReaderPosition current, List external, IEnumerator tagsIterator, HashSet tagsStarts)
+ {
+ var openTag = tagsIterator.Current;
+ current.Position = openTag.SkipTag(openTag.TagStart);
+ var isContextCorrect = openTag.AcceptIfContextCorrect(current.Position);
+ var nested = new List();
+ while (current.Position < markdownText.Length && !openTag.AcceptIfContextEnd(current.Position))
+ {
+ if (tagsStarts.Contains(current.Position))
+ {
+ if (tagsIterator.MoveNext())
+ {
+ var newTag = CreateTag(current, [openTag], tagsIterator, tagsStarts);
+ if (newTag.IsTagClosed)
+ {
+ nested.Add(newTag);
+ }
+ }
+ }
+ else
+ {
+ isContextCorrect = isContextCorrect && openTag.AcceptIfContextCorrect(current.Position);
+ current.Position++;
+ }
+ }
+
+ if ((external.Count == 0 || Tag.IsNestedTagWorks(external[^1].TagType, openTag.TagType)) && isContextCorrect)
+ {
+ openTag.TryCloseTag(current.Position, markdownText, out var tagEnd, nested);
+ current.Position = tagEnd;
+ }
+ else
+ {
+ current.Position = openTag is PairTag ? openTag.SkipTag(current.Position) : current.Position;
+ }
+
+ return openTag;
+ }
+
+ private Tag GetOpenTag(int tagStart, string markdownText, out int contextStart)
+ {
+ var tagBegin = markdownText[tagStart];
+ var tags = tagCreators[tagBegin];
+ var orderedTags = tags.Keys.OrderByDescending(k => k.Length);
+ foreach (var tag in orderedTags)
+ {
+ if (tag.Length + tagStart <= markdownText.Length
+ && markdownText.Substring(tagStart, tag.Length) == tag)
+ {
+ contextStart = tagStart + tag.Length;
+ var createOpenTag = tags[tag];
+ return createOpenTag(markdownText, tagStart);
+ }
+ }
+ throw new InvalidOperationException("Попытка получить несуществующий тег");
+ }
+
+ public List Tokenize(string markdownText)
+ {
+ var tokens = new List();
+ for (int i = 0; i < markdownText.Length; i++)
+ {
+ if (tagCreators.ContainsKey(markdownText[i]))
+ {
+ var tag = GetOpenTag(i, markdownText, out var contextStart);
+ tokens.Add(tag);
+ i = contextStart - 1;
+ if (tag is Escape) i++;
+ }
+ }
+ return tokens;
+ }
+
+ public List FindCorrectTags(List tokens)
+ {
+ var result = MatchTags(tokens);
+ var closedTags = result.Closed;
+ var unclosedPairTag = result.Unclosed;
+ closedTags.AddRange(new UnclosedPairTagsRules().Apply(unclosedPairTag.Reverse().ToList()));
+ return closedTags;
+ }
+
+ public (List Closed, Stack Unclosed) MatchTags(List possibleTags)
+ {
+ var closedTags = new List();
+ var stack = new Stack();
+
+ for (int i = 0; i < possibleTags.Count; i++)
+ {
+ var tag = possibleTags[i];
+ if (tag is not PairTag)
+ {
+ closedTags.Add(tag);
+ }
+ else if (stack.Count == 0 || stack.Peek().TagType != tag.TagType)
+ {
+ stack.Push(tag);
+ }
+ else
+ {
+ var startTag = stack.Pop();
+
+ closedTags.Add(startTag);
+ }
+ }
+ return (closedTags, stack);
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Program.cs b/cs/Markdown/Program.cs
new file mode 100644
index 000000000..3729f8b92
--- /dev/null
+++ b/cs/Markdown/Program.cs
@@ -0,0 +1,10 @@
+namespace Markdown
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+
+ }
+ }
+}
diff --git a/cs/Markdown/Tags/Bold.cs b/cs/Markdown/Tags/Bold.cs
new file mode 100644
index 000000000..45bffeb83
--- /dev/null
+++ b/cs/Markdown/Tags/Bold.cs
@@ -0,0 +1,18 @@
+namespace Markdown.Tags;
+
+public class Bold(string mdText, int tagStart) : PairTag(mdText, tagStart)
+{
+ protected override string MdTag => "__";
+ protected override string HtmlTag => "strong";
+
+ public override MdTagType TagType => MdTagType.Bold;
+
+ public override void TryCloseTag(int contextEnd, string sourceMdText, out int tagEnd, List? nested = null)
+ {
+ tagEnd = contextEnd + MdTag.Length;
+ if (contextEnd != TagStart + MdTag.Length)
+ {
+ base.TryCloseTag(contextEnd, sourceMdText, out tagEnd, nested);
+ }
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/ContextRules/IContextRule.cs b/cs/Markdown/Tags/ContextRules/IContextRule.cs
new file mode 100644
index 000000000..c27b8c67b
--- /dev/null
+++ b/cs/Markdown/Tags/ContextRules/IContextRule.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Markdown.Tags.ContextRules
+{
+ public interface IContextRule
+ {
+ public bool IsContextCorrect(ReadOnlySpan context, int currentPosition, string tag) => true;
+
+ public bool IsContextEnd(ReadOnlySpan context, int currentPosition, string tag) => false;
+ }
+}
diff --git a/cs/Markdown/Tags/ContextRules/PairTagRule.cs b/cs/Markdown/Tags/ContextRules/PairTagRule.cs
new file mode 100644
index 000000000..e9e4a2d39
--- /dev/null
+++ b/cs/Markdown/Tags/ContextRules/PairTagRule.cs
@@ -0,0 +1,24 @@
+namespace Markdown.Tags.ContextRules
+{
+ internal class PairTagRule : IContextRule
+ {
+ public virtual bool IsContextCorrect(ReadOnlySpan context, int currentPosition, string tag) => true;
+
+ public virtual bool IsContextEnd(ReadOnlySpan context, int currentPosition, string tag)
+ {
+ return HaveFoundCloseTag(context, currentPosition, tag)
+ && IsLastContextSymbolNotSpace(context, currentPosition);
+ }
+
+ protected bool HaveFoundCloseTag(ReadOnlySpan context, int currentPosition, string tag)
+ {
+ return currentPosition + tag.Length <= context.Length
+ && context.Slice(currentPosition, tag.Length).ToString() == tag;
+ }
+ protected bool IsLastContextSymbolNotSpace(ReadOnlySpan context, int currentPosition)
+ {
+ return currentPosition > 0
+ && context[currentPosition - 1] != ' ';
+ }
+ }
+}
diff --git a/cs/Markdown/Tags/ContextRules/PairTagSelectFewWordsRule.cs b/cs/Markdown/Tags/ContextRules/PairTagSelectFewWordsRule.cs
new file mode 100644
index 000000000..4aadbcb77
--- /dev/null
+++ b/cs/Markdown/Tags/ContextRules/PairTagSelectFewWordsRule.cs
@@ -0,0 +1,18 @@
+namespace Markdown.Tags.ContextRules
+{
+ internal class PairTagSelectFewWordsRule : PairTagRule
+ {
+ public override bool IsContextEnd(ReadOnlySpan context, int currentPosition, string tag)
+ {
+ return IsStringEndByTag(context, currentPosition, tag)
+ || (base.IsContextEnd(context, currentPosition, tag)
+ && currentPosition + tag.Length < context.Length && !char.IsLetter(context[currentPosition + tag.Length]));
+ }
+
+ private bool IsStringEndByTag(ReadOnlySpan context, int currentPosition, string tag)
+ {
+ return currentPosition + tag.Length == context.Length
+ && context.Slice(currentPosition, tag.Length).ToString() == tag;
+ }
+ }
+}
diff --git a/cs/Markdown/Tags/ContextRules/PairTagSelectPartWordRule.cs b/cs/Markdown/Tags/ContextRules/PairTagSelectPartWordRule.cs
new file mode 100644
index 000000000..aa0a3d23e
--- /dev/null
+++ b/cs/Markdown/Tags/ContextRules/PairTagSelectPartWordRule.cs
@@ -0,0 +1,10 @@
+namespace Markdown.Tags.ContextRules
+{
+ internal class PairTagSelectPartWordRule : PairTagRule
+ {
+ public override bool IsContextCorrect(ReadOnlySpan context, int currentPosition, string tag)
+ {
+ return base.IsContextCorrect(context, currentPosition, tag) && char.IsLetter(context[currentPosition]);
+ }
+ }
+}
diff --git a/cs/Markdown/Tags/ContextRules/UnderscoreTagRule.cs b/cs/Markdown/Tags/ContextRules/UnderscoreTagRule.cs
new file mode 100644
index 000000000..6a551013d
--- /dev/null
+++ b/cs/Markdown/Tags/ContextRules/UnderscoreTagRule.cs
@@ -0,0 +1,16 @@
+namespace Markdown.Tags.ContextRules
+{
+ internal class UnderscoreTagRule : IContextRule
+ {
+ public bool IsContextCorrect(ReadOnlySpan context, int currentPosition, string tag)
+ {
+ return !char.IsDigit(context[currentPosition])
+ && IsFirstContextSymbolNotSpace(context);
+ }
+
+ private bool IsFirstContextSymbolNotSpace(ReadOnlySpan context)
+ {
+ return context.Length > 0 && context[0] != ' ';
+ }
+ }
+}
diff --git a/cs/Markdown/Tags/Escape.cs b/cs/Markdown/Tags/Escape.cs
new file mode 100644
index 000000000..8dbf743f7
--- /dev/null
+++ b/cs/Markdown/Tags/Escape.cs
@@ -0,0 +1,38 @@
+namespace Markdown.Tags;
+
+public class Escape(string markdownText, int tagStart) : Tag(markdownText, tagStart)
+{
+ protected override string MdTag => "\\";
+ protected override string HtmlTag => "";
+ private Dictionary specSymbolsRendering = new Dictionary {
+ { "n", "\n" }, { "t", "\t" } };
+ public override MdTagType TagType => MdTagType.Escape;
+
+ public override string RenderToHtml()
+ {
+ var value = Context.GetValue();
+ return $"{(specSymbolsRendering.ContainsKey(value) ? specSymbolsRendering[value] : value)}";
+ }
+
+ public override bool AcceptIfContextEnd(int currentPosition)
+ {
+ return currentPosition > TagStart + 1;
+ }
+
+ public override bool AcceptIfContextCorrect(int currentPosition)
+ {
+ var current = MarkdownText[currentPosition];
+ return base.AcceptIfContextCorrect(currentPosition)
+ && currentPosition < MarkdownText.Length
+ && (Md.MdTags.ContainsKey(current) || specSymbolsRendering.ContainsKey(current.ToString()));
+ }
+
+ public override void TryCloseTag(int contextEnd, string sourceMdText, out int tagEnd, List? nested = null)
+ {
+ Context = Md.MdTags.ContainsKey(MarkdownText[TagStart + 1]) || specSymbolsRendering.ContainsKey(MarkdownText[TagStart + 1].ToString())
+ ? new Token(TagStart + 1, MarkdownText, 1)
+ : new Token(TagStart, MarkdownText, 1);
+ tagEnd = contextEnd;
+ TagEnd = tagEnd;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/Header.cs b/cs/Markdown/Tags/Header.cs
new file mode 100644
index 000000000..43f1d534f
--- /dev/null
+++ b/cs/Markdown/Tags/Header.cs
@@ -0,0 +1,27 @@
+namespace Markdown.Tags;
+
+public class Header(string markdownText, int tagStart) : Tag(markdownText, tagStart)
+{
+ protected override string MdTag => "# ";
+ protected override string HtmlTag => "h1";
+ public override MdTagType TagType => MdTagType.Header;
+
+ public override bool AcceptIfContextEnd(int currentPosition)
+ {
+ return currentPosition > MarkdownText.Length - 1 || MarkdownText[currentPosition] == '\n';
+ }
+
+ public override bool AcceptIfContextCorrect(int currentPosition)
+ {
+ return TagStart == 0 || TagStart - 1 == '\n';
+ }
+
+ public override void TryCloseTag(int contextEnd, string sourceMdText, out int tagEnd, List? nested = null)
+ {
+ var contextStart = TagStart + MdTag.Length;
+ tagEnd = contextEnd == sourceMdText.Length - 1 ? contextEnd : contextEnd + 1;
+ Context = new Token(contextStart, sourceMdText, contextEnd - contextStart);
+ NestedTags = nested;
+ TagEnd = tagEnd;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/Italic.cs b/cs/Markdown/Tags/Italic.cs
new file mode 100644
index 000000000..dbd719e96
--- /dev/null
+++ b/cs/Markdown/Tags/Italic.cs
@@ -0,0 +1,10 @@
+using Markdown.Tags.ContextRules;
+
+namespace Markdown.Tags;
+
+public class Italic(string mdText, int tagStart) : PairTag(mdText, tagStart)
+{
+ protected override string MdTag => "_";
+ protected override string HtmlTag => "em";
+ public override MdTagType TagType => MdTagType.Italic;
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/MdTagType.cs b/cs/Markdown/Tags/MdTagType.cs
new file mode 100644
index 000000000..cc678e953
--- /dev/null
+++ b/cs/Markdown/Tags/MdTagType.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Markdown.Tags
+{
+ public enum MdTagType
+ {
+ Bold, Escape, Header, Italic
+ }
+}
diff --git a/cs/Markdown/Tags/PairTag.cs b/cs/Markdown/Tags/PairTag.cs
new file mode 100644
index 000000000..9b343e84c
--- /dev/null
+++ b/cs/Markdown/Tags/PairTag.cs
@@ -0,0 +1,41 @@
+using Markdown.Tags.ContextRules;
+
+namespace Markdown.Tags
+{
+ public abstract class PairTag: Tag
+ {
+ protected List Rules = [];
+ protected bool isClose = false;
+
+ protected PairTag(string markdownText, int tagStart): base(markdownText, tagStart)
+ {
+ MarkdownText = markdownText;
+ Rules = [new UnderscoreTagRule(), new PairTagSelectPartWordRule()];
+ }
+ public override void TryCloseTag(int contextEnd, string sourceMdText, out int tagEnd, List? nested = null)
+ {
+ tagEnd = contextEnd + MdTag.Length;
+ if (isClose)
+ {
+ base.TryCloseTag(contextEnd, sourceMdText, out tagEnd, nested);
+ }
+ }
+ public override bool AcceptIfContextEnd(int currentPosition)
+ {
+ var contextStart = TagStart + MdTag.Length;
+ isClose = Rules.Any(rule => rule.IsContextEnd(MarkdownText.AsSpan().Slice(contextStart), currentPosition - contextStart, MdTag));
+ return isClose;
+ }
+
+ public override bool AcceptIfContextCorrect(int currentPosition)
+ {
+ var contextStart = TagStart + MdTag.Length;
+ if (!char.IsLetter(MarkdownText[currentPosition]) && (TagStart == 0 || !char.IsLetter(MarkdownText[TagStart - 1])))
+ {
+ Rules = [ new UnderscoreTagRule(), new PairTagSelectFewWordsRule()];
+ };
+
+ return Rules.All(rule => rule.IsContextCorrect(MarkdownText.AsSpan().Slice(contextStart), currentPosition - contextStart, MdTag));
+ }
+ }
+}
diff --git a/cs/Markdown/Tags/Tag.cs b/cs/Markdown/Tags/Tag.cs
new file mode 100644
index 000000000..bd6840e9e
--- /dev/null
+++ b/cs/Markdown/Tags/Tag.cs
@@ -0,0 +1,75 @@
+using System.Text;
+using Microsoft.VisualBasic.CompilerServices;
+
+namespace Markdown.Tags;
+
+public abstract class Tag(string MarkdownText, int TagStart)
+{
+ private static readonly Dictionary> nestedRules = new()
+ {
+ { MdTagType.Header, new () { MdTagType.Bold, MdTagType.Italic, MdTagType.Escape}},
+ { MdTagType.Bold, new () { MdTagType.Italic, MdTagType.Escape}},
+ { MdTagType.Italic, new () { MdTagType.Escape} }
+ };
+
+ public static bool IsNestedTagWorks(MdTagType external, MdTagType nested)
+ {
+ return nestedRules.ContainsKey(external) && nestedRules[external].Contains(nested);
+ }
+
+ public bool IsTagClosed { get; protected set; }
+ public int TagStart { get; } = TagStart;
+ public int TagEnd;
+ public string MarkdownText = MarkdownText;
+ public abstract MdTagType TagType { get; }
+ private Token? context;
+ protected List? NestedTags;
+ protected bool IsContextCorrect = true;
+ protected Token Context
+ {
+ get => context ?? throw new IncompleteInitialization();
+ set
+ {
+ if (!IsTagClosed)
+ {
+ IsTagClosed = true;
+ context = value;
+ }
+ }
+ }
+ protected abstract string MdTag { get; }
+ protected abstract string HtmlTag { get; }
+
+ public virtual int SkipTag(int position) => position + MdTag.Length;
+ public virtual string RenderToHtml()
+ {
+ var result = new StringBuilder();
+ var tags = NestedTags?.ToDictionary(t => t.TagStart) ?? [];
+ for (var i = TagStart + MdTag.Length; i < Math.Min(Context.Position + Context.Length, MarkdownText.Length); )
+ {
+ if (tags.ContainsKey(i))
+ {
+ var nested = tags[i];
+ result.Append(nested.RenderToHtml());
+ i = nested.TagEnd;
+ }
+ else
+ {
+ result.Append(MarkdownText[i]);
+ i++;
+ }
+ }
+ return $"<{HtmlTag}>{result}{HtmlTag}>";
+ }
+ public virtual void TryCloseTag(int contextEnd, string sourceMdText, out int tagEnd, List? nested = null)
+ {
+ var contextStart = TagStart + MdTag.Length;
+ tagEnd = contextEnd + MdTag.Length;
+ Context = new Token(contextStart, sourceMdText, contextEnd - contextStart);
+ TagEnd = tagEnd;
+ NestedTags = nested;
+ }
+
+ public abstract bool AcceptIfContextEnd(int currentPosition);
+ public virtual bool AcceptIfContextCorrect(int currentPosition) => true;
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/TagsInteraction/UnclosedPairTagRules.cs b/cs/Markdown/Tags/TagsInteraction/UnclosedPairTagRules.cs
new file mode 100644
index 000000000..d1de68d21
--- /dev/null
+++ b/cs/Markdown/Tags/TagsInteraction/UnclosedPairTagRules.cs
@@ -0,0 +1,35 @@
+namespace Markdown.Tags.TagsInteraction
+{
+ public class UnclosedPairTagsRules
+ {
+ public List Apply(List unclosedTags)
+ {
+ var closedTags = new List();
+ var current = unclosedTags.Count - unclosedTags.Count % 4;
+ if (current < unclosedTags.Count)
+ {
+ var tag = unclosedTags[current];
+ if (unclosedTags.Count - current == 2)
+ {
+ if (tag is Italic)
+ closedTags.Add(new Italic(tag.MarkdownText, tag.TagStart));
+ else
+ closedTags.Add(new Italic(tag.MarkdownText, tag.TagStart + 1));
+ }
+ else if (unclosedTags.Count - current == 3)
+ {
+ if (tag is Italic)
+ {
+ closedTags.Add(new Italic(tag.MarkdownText, tag.TagStart));
+ closedTags.Add(new Italic(tag.MarkdownText, tag.TagStart + 1));
+ }
+ else
+ {
+ closedTags.Add(new Bold(tag.MarkdownText, tag.TagStart));
+ }
+ }
+ }
+ return closedTags;
+ }
+ }
+}
diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs
new file mode 100644
index 000000000..e23a947a5
--- /dev/null
+++ b/cs/Markdown/Token.cs
@@ -0,0 +1,18 @@
+namespace Markdown;
+
+public record Token
+{
+ private readonly string source;
+ private readonly string value;
+ public Token(int position, string source, int length)
+ {
+ Position = position;
+ Length = length;
+ value = source.Substring(Position, Length);
+ }
+
+ public int Position { get; }
+ public int Length { get; }
+
+ public string GetValue() => value;
+}
\ No newline at end of file
diff --git a/cs/MarkdownTests/MarkdownTests.csproj b/cs/MarkdownTests/MarkdownTests.csproj
new file mode 100644
index 000000000..9e6c35fd8
--- /dev/null
+++ b/cs/MarkdownTests/MarkdownTests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cs/MarkdownTests/MdParserTests.cs b/cs/MarkdownTests/MdParserTests.cs
new file mode 100644
index 000000000..ddab7b9b0
--- /dev/null
+++ b/cs/MarkdownTests/MdParserTests.cs
@@ -0,0 +1,58 @@
+using FluentAssertions;
+using Markdown;
+
+namespace MarkdownTests
+{
+ public class MdParserTests
+ {
+ public static IEnumerable DifferentTagsParseWithoutNestedAndIntersection
+ {
+ get
+ {
+ yield return new TestCaseData("__ ____",
+ new []
+ {
+ "", ""
+ })
+ .SetName("WhenHasTwoTagsWithoutIntersection");
+ yield return new TestCaseData("\\_ \\_",
+ new[]
+ {
+ "_", "_"
+ })
+ .SetName("WhenEscapeTagEscapeItalicTagStartAndEnd");
+ yield return new TestCaseData("\\\\_ _",
+ new[]
+ {
+ "\\", " "
+ })
+ .SetName("WhenEscapeTagEscapeItself");
+ }
+ }
+ [TestCase("# Hello World!\nHi, Mari!", "Hello World!
", TestName = "HeaderTag")]
+ [TestCase("dsf\\_sdf", "_", TestName = "EscapeTag")]
+ [TestCase("__ __", " ", TestName = "BoldTag")]
+ [TestCase("_ _", " ", TestName = "ItalicTag")]
+ [TestCase("# Hello World!", "Hello World!
", TestName = "HeaderTagWithoutParagraphEndSymbol")]
+ public void MdParser_ShouldCorrectParse(string mdText, string expected)
+ {
+ var parser = new MdParser(mdText, Md.MdTags);
+
+ var tags = parser.GetTags();
+ var toHtml = tags[0].RenderToHtml();
+
+ toHtml.Should().Be(expected);
+ }
+
+ [TestCaseSource(nameof(DifferentTagsParseWithoutNestedAndIntersection))]
+ public void MdParser_ShouldCorrectParseDifferentTags(string mdText, string[] expectedTagsToHtml)
+ {
+ var parser = new MdParser(mdText, Md.MdTags);
+
+ var tags = parser.GetTags().Select(t => t.RenderToHtml());
+
+ tags.Should().BeEquivalentTo(expectedTagsToHtml);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/cs/MarkdownTests/MdRulesForNestedTagsTests.cs b/cs/MarkdownTests/MdRulesForNestedTagsTests.cs
new file mode 100644
index 000000000..d5d736c7b
--- /dev/null
+++ b/cs/MarkdownTests/MdRulesForNestedTagsTests.cs
@@ -0,0 +1,35 @@
+using FluentAssertions;
+using Markdown;
+using Markdown.Tags;
+
+namespace MarkdownTests
+{
+ public class MdRulesForNestedTagsTests
+ {
+ [Test]
+ public void InsideBold_ItalicShouldWork()
+ {
+ var result = Tag.IsNestedTagWorks(MdTagType.Bold, MdTagType.Italic);
+
+ result.Should().BeTrue();
+ }
+ [Test]
+ public void InsideItalic_BoldShouldNotWork()
+ {
+ var result = Tag.IsNestedTagWorks(MdTagType.Italic, MdTagType.Bold);
+
+ result.Should().BeFalse();
+ }
+
+ [Test]
+ public void InsideHeader_AllTagWorks()
+ {
+ var result = new [] {
+ Tag.IsNestedTagWorks(MdTagType.Header, MdTagType.Bold),
+ Tag.IsNestedTagWorks(MdTagType.Header, MdTagType.Italic),
+ Tag.IsNestedTagWorks(MdTagType.Header, MdTagType.Escape)};
+
+ result.Should().AllBeEquivalentTo(true);
+ }
+ }
+}
diff --git a/cs/MarkdownTests/MdTests.cs b/cs/MarkdownTests/MdTests.cs
new file mode 100644
index 000000000..9f7e84c8f
--- /dev/null
+++ b/cs/MarkdownTests/MdTests.cs
@@ -0,0 +1,90 @@
+using Markdown;
+using FluentAssertions;
+
+namespace MarkdownTests
+{
+ public class MdTests
+ {
+ public static IEnumerable NestedTagsTestCases
+ {
+ get
+ {
+ yield return new TestCaseData("Внутри __двойного выделения _одинарное_ тоже__ работает.",
+ "Внутри двойного выделения одинарное тоже работает.")
+ .SetName("WhenItalicTagInBold");
+ yield return new TestCaseData("внутри _одинарного __двойное__ не_ работает.",
+ "внутри одинарного __двойное__ не работает.")
+ .SetName("NotRenderBold_WhenBoldInItalic");
+ yield return new TestCaseData("# Заголовок __с _разными_ символами__",
+ "Заголовок с разными символами
")
+ .SetName("WhenManyTagsInHeader");
+ yield return new TestCaseData("_этот текст весь будет\\_ выделен тегом_",
+ "этот текст весь будет_ выделен тегом")
+ .SetName("EscapeSymbol_WhenEscapeNestedInItalic");
+ yield return new TestCaseData("__этот текст весь _будет\\__ выделен тегом__",
+ "этот текст весь будет_ выделен тегом")
+ .SetName("EscapeSymbol_WhenEscapeNestedInItalicAndStrong");
+ yield return new TestCaseData("В случае __пересечения _двойных__ и одинарных_ подчерков",
+ "В случае __пересечения _двойных__ и одинарных_ подчерков")
+ .SetName("NotRenderTag_WhenTheyHaveIntersection");
+ yield return new TestCaseData("н_ачал_о",
+ "начало")
+ .SetName("RenderTag_WhenSelectPartOfWordMiddle");
+ yield return new TestCaseData("_на_чало",
+ "начало")
+ .SetName("RenderTag_WhenSelectStartOfWord");
+ yield return new TestCaseData("начал_о_",
+ "начало")
+ .SetName("RenderTag_WhenSelectEndOfWord");
+ yield return new TestCaseData("__FirstTag__, __SecondTag__",
+ "FirstTag, SecondTag")
+ .SetName("RenderTag_WhenFewDifferentTags1");
+ yield return new TestCaseData("__AaAa__, _b_",
+ "AaAa, b")
+ .SetName("RenderTag_WhenFewDifferentTags2");
+ yield return new TestCaseData("\\n",
+ "\n")
+ .SetName("LineEndSymbolNotEscapeTag");
+ yield return new TestCaseData("__AaAa_b__",
+ "AaAa_b")
+ .SetName("NotRenderItalicTag_WhenUnpairItalicAndPairBold");
+ }
+ }
+ public static IEnumerable IncorrectTagsTests
+ {
+ get
+ {
+ yield return new TestCaseData("__Непарные_ символы в рамках одного абзаца не считаются выделением.",
+ "_Непарные символы в рамках одного абзаца не считаются выделением.")
+ .SetName("UnpairSymbolsNotATags");
+ yield return new TestCaseData("эти_ подчерки_ не считаются",
+ "эти_ подчерки_ не считаются")
+ .SetName("ShouldNotRenderTag_WhenUnderscoresHaveSpaceAfterItSelf");
+ yield return new TestCaseData("не считается # ",
+ "не считается # ")
+ .SetName("ShouldNotRenderTag_WhenHeaderNotInStartParagraph");
+ yield return new TestCaseData("ра_зных сл_овах ра__зных сл__овах не работает",
+ "ра_зных сл_овах ра__зных сл__овах не работает")
+ .SetName("ShouldNotRenderTag_WhenPairTagSelectPartOfDifferentWords");
+ yield return new TestCaseData("_вот это будет \\д_ выделено тегом_",
+ "вот это будет \\д выделено тегом_")
+ .SetName("ShouldRenderTag_WhenEscapeWithOneSymbol");
+ yield return new TestCaseData("____",
+ "____")
+ .SetName("ShouldNotRenderBoldTag_WhenInsideIsEmptyString");
+
+ }
+ }
+
+ [TestCaseSource(nameof(NestedTagsTestCases))]
+ [TestCaseSource(nameof(IncorrectTagsTests))]
+ public void Render_Should(string mdText, string expectedTagsToHtml)
+ {
+ var md = new Md();
+
+ var toHtml = md.Render(mdText);
+
+ toHtml.Should().BeEquivalentTo(expectedTagsToHtml);
+ }
+ }
+}
diff --git a/cs/MarkdownTests/Tags/BoldTests.cs b/cs/MarkdownTests/Tags/BoldTests.cs
new file mode 100644
index 000000000..cfd7d2c26
--- /dev/null
+++ b/cs/MarkdownTests/Tags/BoldTests.cs
@@ -0,0 +1,40 @@
+using Markdown.Tags;
+using FluentAssertions;
+
+namespace MarkdownTests.Tags
+{
+ public class BoldTests
+ {
+ [Test]
+ public void BoldTag_ShouldCorrectCalculateTagEnd()
+ {
+ var openTag = new Bold("__Вы__a", 0);
+
+ openTag.TryCloseTag(3, "__Вы__a", out int tagEnd);
+
+ tagEnd.Should().Be(5);
+ }
+
+
+ [TestCase("__12__", false, TestName = "ReturnFalse_WhenInsideDigits")]
+ [TestCase("__нач__але", true, TestName = "ReturnTrue_WhenSelectPartOfWord")]
+ [TestCase("в ра__зных сл__овах", false, TestName = "ReturnFalse_WhenSelectPartsOfDifferentWords1")]
+ [TestCase("в раз__ных словах__", false, TestName = "ReturnFalse_WhenSelectPartsOfDifferentWords3")]
+ [TestCase("в __разных словах__", true, TestName = "ReturnTrue_WhenSelectWholeWords")]
+ [TestCase("в __ разных словах__", false, TestName = "ReturnFalse_WhenHasSpaceAfterOpenTag")]
+ public void AcceptIfContextCorrect_Should(string mdText, bool expected)
+ {
+ var tagStart = mdText.IndexOf('_');
+ var contextEnd = mdText.LastIndexOf('_') - 2;
+ var contextStart = tagStart + 2;
+ var openTag = new Bold(mdText, tagStart);
+ var isContextCorrect = true;
+ for (var i = contextStart; i <= contextEnd; i++)
+ {
+ isContextCorrect = isContextCorrect && openTag.AcceptIfContextCorrect(i);
+ }
+
+ isContextCorrect.Should().Be(expected);
+ }
+ }
+}
diff --git a/cs/MarkdownTests/Tags/EscapeTests.cs b/cs/MarkdownTests/Tags/EscapeTests.cs
new file mode 100644
index 000000000..f31f7ebdd
--- /dev/null
+++ b/cs/MarkdownTests/Tags/EscapeTests.cs
@@ -0,0 +1,30 @@
+using Markdown.Tags;
+using FluentAssertions;
+
+namespace MarkdownTests.Tags
+{
+ public class EscapeTests
+ {
+ [Test]
+ public void EscapeTag_ContextShould_ContainsOneSymbol()
+ {
+ var openTag = new Escape("\\_", 0);
+
+ openTag.TryCloseTag(1, "\\_", out int tagEnd);
+
+ tagEnd.Should().Be(1);
+ }
+
+ [TestCase("\\_", 0, true, TestName = "EscapeAnotherTag")]
+ [TestCase("\\\\", 0, true, TestName = "EscapeItSelf")]
+ [TestCase("\\a", 0, false, TestName = "HaveNoSymbolsForEscape")]
+ public void EscapeTag_HaveCorrectContext_WhenEscapeMarkupSymbol(string markdownText, int tagStart, bool expected)
+ {
+ var openTag = new Escape(markdownText, tagStart);
+
+ var isContextCorrect = openTag.AcceptIfContextCorrect(1);
+
+ isContextCorrect.Should().Be(expected);
+ }
+ }
+}
diff --git a/cs/MarkdownTests/Tags/HeaderTests.cs b/cs/MarkdownTests/Tags/HeaderTests.cs
new file mode 100644
index 000000000..2dc63a9c0
--- /dev/null
+++ b/cs/MarkdownTests/Tags/HeaderTests.cs
@@ -0,0 +1,19 @@
+using FluentAssertions;
+using Markdown.Tags;
+
+namespace MarkdownTests.Tags
+{
+ public class HeaderTests
+ {
+ [TestCase("#Вы fhjg\n", 7, 8)]
+ [TestCase("#Вы fhjg", 7, 7, TestName = "AndHasNotSymbolForParagraphEnd")]
+ public void HeaderTag_ShouldClose_WhenParagraphEnd(string mdText, int contextEnd, int expected)
+ {
+ var openTag = new Header(mdText, 0);
+
+ openTag.TryCloseTag(contextEnd, mdText, out var tagEnd);
+
+ tagEnd.Should().Be(expected);
+ }
+ }
+}
diff --git a/cs/MarkdownTests/Tags/ItalicTests.cs b/cs/MarkdownTests/Tags/ItalicTests.cs
new file mode 100644
index 000000000..4d89431c1
--- /dev/null
+++ b/cs/MarkdownTests/Tags/ItalicTests.cs
@@ -0,0 +1,40 @@
+using FluentAssertions;
+using Markdown.Tags;
+
+namespace MarkdownTests.Tags
+{
+ public class ItalicTests
+ {
+ [Test]
+ public void ItalicTag_ShouldCorrectCalculateTagEnd()
+ {
+ var openTag = new Italic("_Вы_", 0);
+
+ openTag.TryCloseTag(2, "_Вы_", out int tagEnd, []);
+
+ tagEnd.Should().Be(3);
+ }
+
+ [TestCase("_12_", false, TestName = "ReturnFalse_WhenInsideDigits")]
+ [TestCase("_нач_але", true, TestName = "ReturnTrue_WhenSelectPartOfWord")]
+ [TestCase(" _нач_але", true, TestName = "ReturnTrue_WhenSelectPartOfWordAfterSpace")]
+ [TestCase("в ра_зных сл_овах", false, TestName = "ReturnFalse_WhenSelectPartsOfDifferentWords1")]
+ [TestCase("в раз_ных словах_", false, TestName = "ReturnFalse_WhenSelectPartsOfDifferentWords3")]
+ [TestCase("в _разных словах_", true, TestName = "ReturnTrue_WhenSelectWholeWords")]
+ [TestCase("в _ разных словах_", false, TestName = "ReturnFalse_WhenHasSpaceAfterOpenTag")]
+ public void AcceptIfContextCorrect_Should(string mdText, bool expected)
+ {
+ var tagStart = mdText.IndexOf('_');
+ var contextEnd = mdText.LastIndexOf('_') - 1;
+ var contextStart = tagStart + 1;
+ var openTag = new Italic(mdText, tagStart);
+ var isContextCorrect = true;
+ for (var i = contextStart; i <= contextEnd; i++)
+ {
+ isContextCorrect = isContextCorrect && openTag.AcceptIfContextCorrect(i);
+ }
+
+ isContextCorrect.Should().Be(expected);
+ }
+ }
+}
diff --git a/cs/clean-code.sln b/cs/clean-code.sln
index 2206d54db..18322bfd2 100644
--- a/cs/clean-code.sln
+++ b/cs/clean-code.sln
@@ -1,13 +1,17 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
+# Visual Studio Version 17
+VisualStudioVersion = 17.9.34902.65
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chess", "Chess\Chess.csproj", "{DBFBE40E-EE0C-48F4-8763-EBD11C960081}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chess", "Chess\Chess.csproj", "{DBFBE40E-EE0C-48F4-8763-EBD11C960081}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigit\ControlDigit.csproj", "{B06A4B35-9D61-4A63-9167-0673F20CA989}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlDigit", "ControlDigit\ControlDigit.csproj", "{B06A4B35-9D61-4A63-9167-0673F20CA989}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown", "Markdown\Markdown.csproj", "{3028CB9F-9163-473E-987F-4B5EE2A2499E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownTests", "MarkdownTests\MarkdownTests.csproj", "{9F8A3F5D-4730-4D64-90F6-2462F2D63CB7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,5 +31,19 @@ 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
+ {3028CB9F-9163-473E-987F-4B5EE2A2499E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3028CB9F-9163-473E-987F-4B5EE2A2499E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3028CB9F-9163-473E-987F-4B5EE2A2499E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3028CB9F-9163-473E-987F-4B5EE2A2499E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F8A3F5D-4730-4D64-90F6-2462F2D63CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F8A3F5D-4730-4D64-90F6-2462F2D63CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F8A3F5D-4730-4D64-90F6-2462F2D63CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F8A3F5D-4730-4D64-90F6-2462F2D63CB7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {53407E3E-AA1E-470A-B14B-CDA55F1D274E}
EndGlobalSection
EndGlobal
diff --git a/cs/clean-code.sln.DotSettings b/cs/clean-code.sln.DotSettings
index 135b83ecb..11348e23c 100644
--- a/cs/clean-code.sln.DotSettings
+++ b/cs/clean-code.sln.DotSettings
@@ -1,4 +1,4 @@
-
+
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" />
True