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}"; + } + 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