diff --git a/cs/Markdown/BinaryTrees.cs b/cs/Markdown/BinaryTrees.cs new file mode 100644 index 000000000..2918dd1b5 --- /dev/null +++ b/cs/Markdown/BinaryTrees.cs @@ -0,0 +1,128 @@ +using System.Collections; + +namespace Markdown; + +public class BinaryTree : IEnumerable + where T : IComparable +{ + private TreeNode Root; + public void Add(T key) + { + if (Equals(Root, null)) { Root = new TreeNode(key, null); return; } + + var currentSubtree = Root; + + while (true) + { + if (key.CompareTo(currentSubtree.Value) >= 0) + { + currentSubtree.HeightOfRight++; + if (currentSubtree.Right == null) { currentSubtree.Right = new TreeNode(key, currentSubtree); return; } + else currentSubtree = currentSubtree.Right; + } + else + { + currentSubtree.HeightOfLeft++; + if (currentSubtree.Left == null) { currentSubtree.Left = new TreeNode(key, currentSubtree); return; } + else currentSubtree = currentSubtree.Left; + } + } + } + + public bool Contains(T key) + { + var currentSubtree = Root; + + while (!Equals(currentSubtree, null)) + { + if (key.CompareTo(currentSubtree.Value) == 0) + return true; + + if (key.CompareTo(currentSubtree.Value) > 0) + currentSubtree = currentSubtree.Right; + else currentSubtree = currentSubtree.Left; + } + + return false; + } + + public T this[int i] + { + get + { + if (Root.HeightOfRight + Root.HeightOfLeft < i || i < 0) + throw new IndexOutOfRangeException(); + + var currentSubtree = Root; + var index = 0; + + while (true) + { + if (currentSubtree.HeightOfLeft + index == i) return currentSubtree.Value; + else if (currentSubtree.HeightOfLeft + index > i) + currentSubtree = currentSubtree.Left; + else if (currentSubtree.HeightOfLeft < i) + { + index += currentSubtree.HeightOfLeft + 1; + currentSubtree = currentSubtree.Right; + } + } + } + } + + public IEnumerator GetEnumerator() + { + if (Root == null) yield break; + + foreach (var subtree in Root) + yield return subtree.Value; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public class TreeNode : IEnumerable + { + public T Value; + public int HeightOfLeft { get; set; } + public int HeightOfRight { get; set; } + + public TreeNode Left, Right, Ancestor; + + public TreeNode(T value, TreeNode ancestor) + { + Value = value; + Ancestor = ancestor; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + var treeNode = this; + + while (!Equals(treeNode.Left, null)) + treeNode = treeNode.Left; + + while (true) + { + yield return treeNode; + + if (treeNode.Right != null) + { + foreach (var tree in treeNode.Right) + yield return tree; + } + + if (treeNode == this) break; + + treeNode = treeNode.Ancestor; + } + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..d17e31785 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..24ca11598 --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,132 @@ +using System.Text; + +namespace Markdown; + +internal class Md +{ + public string Render(string markdown) + { + var markupSpecification = GetMarkupSpecification().ToArray(); + + return RemoveEscapingOfControlSubstrings(PerformTextFormatting(markdown, + FindAllSubstringsForFormatting(markdown, markupSpecification)), markupSpecification); + } + + private IEnumerable GetMarkupSpecification() + { + var result = new List(); + + var invalidSubstring = new List { "__" }; + for (var digit = 1; digit < 10; digit++) + invalidSubstring.Add(digit.ToString()); + result.Add(new TagReplacementSpecification( + invalidSubstring, + "_", "", + null, null, + ["_ "], [ " _", "__" ])); + + result.Add(new TagReplacementSpecification([], + "__", "")); + + result.Add(new SingleReplacementTagSpecification([], + "# ", "

")); + + return result; + } + + private IEnumerable FindAllSubstringsForFormatting(string text, + IEnumerable markupSpecification) + { + foreach (var tagSpecific in markupSpecification) + { + foreach (var fragment in FindAllFragmentsHighlightedByTag(tagSpecific, text)) + yield return fragment; + } + } + + private IEnumerable FindAllFragmentsHighlightedByTag( + TagReplacementSpecification tagSpecific, string text) + { + var index = 0; + text = ".." + text; + var lookingForOpenTag = true; + + for (var i = 0; i < text.Length - tagSpecific.InputOpeningTag.Length - 1; i++) + { + var currentSubstring = text.Substring(i, tagSpecific.InputOpeningTag.Length + 2); + + if (lookingForOpenTag && currentSubstring.EndsWith(tagSpecific.InputOpeningTag) && + i + tagSpecific.InputOpeningTag.Length + 3 < text.Length && + tagSpecific.CheckOpeningTag(text.Substring(i, tagSpecific.InputOpeningTag.Length + 3))) + { + index = i; + lookingForOpenTag = false; + } + else if (!lookingForOpenTag && currentSubstring.EndsWith(tagSpecific.InputClosingTag) && + tagSpecific.CheckClosingTag(currentSubstring)) + { + lookingForOpenTag = true; + var length = i + tagSpecific.InputOpeningTag.Length - index; + + if (length > 2 * tagSpecific.InputOpeningTag.Length) + yield return new TextFragment(index, length, tagSpecific); + } + else + { + lookingForOpenTag = tagSpecific.InvalidSubstringsInMarkup + .Any(currentSubstring.EndsWith) || lookingForOpenTag; + } + } + } + + private string PerformTextFormatting(string text, IEnumerable fragments) + { + if (!fragments.Any()) return text; + + var result = new StringBuilder(); + var endOfLastReplacement = -1; + + foreach (var replacementOptions in GetSortedCollectionTags(fragments)) + { + result.Append(text[(endOfLastReplacement + 1)..replacementOptions.StartIndex]); + result.Append(replacementOptions.NewTag); + endOfLastReplacement = replacementOptions.StartIndex + replacementOptions.OldTag.Length - 1; + } + + if (endOfLastReplacement + 1 != text.Length) + result.Append(text[(endOfLastReplacement + 1)..text.Length]); + + return result.ToString(); + } + + private BinaryTree GetSortedCollectionTags(IEnumerable fragments) + { + var result = new BinaryTree(); + + foreach (var fragment in fragments) + { + result.Add(new TagReplacementOptions( + fragment.Specification.InputOpeningTag, + fragment.Specification.OutputOpeningTag, + fragment.StartIndex)); + result.Add(new TagReplacementOptions( + fragment.Specification.InputClosingTag, + fragment.Specification.OutputClosingTag, + fragment.StartIndex + fragment.Length - fragment.Specification.InputClosingTag.Length)); + } + + return result; + } + + private string RemoveEscapingOfControlSubstrings(string text, IEnumerable tags) + { + foreach (var tag in tags) + { + text = text.Replace('\\' + tag.InputOpeningTag, tag.InputClosingTag); + if (tag.InputClosingTag != tag.InputOpeningTag) + text = text.Replace('\\' + tag.InputClosingTag, tag.InputClosingTag); + } + + return text.Replace(@"\\", "\\");; + } +} \ No newline at end of file diff --git a/cs/Markdown/MdTests.cs b/cs/Markdown/MdTests.cs new file mode 100644 index 000000000..99601bc16 --- /dev/null +++ b/cs/Markdown/MdTests.cs @@ -0,0 +1,101 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown; + + +[TestFixture] +internal class MdTests +{ + private Md md; + [SetUp] + public void InitializeFild() + { + md = new Md(); + } + + [Test] + public void Render_StringEmpty_NoExceptions() + { + var lambda = () => md.Render(string.Empty); + + lambda.Should().NotThrow(); + } + + [TestCase("_12_3", "_12_3")] + [TestCase("_выделяется тегом_", "выделяется тегом")] + [TestCase("эти_ подчерки_ не считаются выделением", "эти_ подчерки_ не считаются выделением", + "За подчерками, начинающими выделение, должен следовать непробельный символ.")] + [TestCase("_нач_але, и в сер_еди_не, и в кон_це._", "начале, и в середине, и в конце.")] + [TestCase("курсив в ра_зных сл_овах не работает", "курсив в ра_зных сл_овах не работает")] + [TestCase("эти _подчерки _не считаются", "эти _подчерки _не считаются", + "Подчерки, заканчивающие выделение, должны следовать за непробельным символом.")] + public void Render_WrappedInSingleUnderscore_WrappedInTagEm(string markdown, string expected, string message = "") + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase("__выделяется тегом__", "выделяется тегом")] + public void Render_WrappedInDoubleUnderscore_WrappedInTagEm(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase(@"\_текст\_", "_текст_")] + [TestCase(@"\_\_не выделяется тегом\_\_", "__не выделяется тегом__")] + [TestCase(@"\\_вот это будет выделено тегом_", @"\вот это будет выделено тегом")] + [TestCase(@"Здесь сим\волы экранирования\ \должны остаться.\", @"Здесь сим\волы экранирования\ \должны остаться.\")] + + public void Render_EscapingCharacters_FormattingIsNotApplied(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase("Внутри __двойного выделения _одинарное_ тоже__ работает", + "Внутри двойного выделения одинарное тоже работает")] + [TestCase("внутри _одинарного __двойное__ не_ работает", "внутри одинарного __двойное__ не работает")] + public void Render_NestedKeywords(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase("__Непарные_ символы", "__Непарные_ символы")] + public void Render_UnpairedFormattingCharacters_FormattingIsNotApplied(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase("__пересечения _двойных__ и одинарных_ подчерков", "__пересечения _двойных__ и одинарных_ подчерков")] + public void Render_IntersectionDoubleAndSingleUnderscores_FormattingIsNotHappening(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase("# Заголовок\n\r текст", "

Заголовок

текст")] + public void Render_Heading_TurnsIntoTagH1(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } + + [TestCase("# Заголовок __с _разными_ символами__\n\r", "

Заголовок с разными символами

")] + public void Render_HeadingWithDifferentKeyCharacters(string markdown, string expected) + { + var actual = md.Render(markdown); + + actual.Should().Be(expected); + } +} diff --git a/cs/Markdown/SingleReplacementTagSpecification.cs b/cs/Markdown/SingleReplacementTagSpecification.cs new file mode 100644 index 000000000..f6936dd60 --- /dev/null +++ b/cs/Markdown/SingleReplacementTagSpecification.cs @@ -0,0 +1,10 @@ +namespace Markdown; + +public class SingleReplacementTagSpecification : TagReplacementSpecification +{ + public SingleReplacementTagSpecification(IEnumerable invalidSubstringsInMarkup, + string inputOpeningTag, string outputOpeningTag) + : base(invalidSubstringsInMarkup, inputOpeningTag, outputOpeningTag, "\n\r") + { + } +} \ No newline at end of file diff --git a/cs/Markdown/TagReplacementOptions.cs b/cs/Markdown/TagReplacementOptions.cs new file mode 100644 index 000000000..59aeef6e4 --- /dev/null +++ b/cs/Markdown/TagReplacementOptions.cs @@ -0,0 +1,28 @@ +namespace Markdown; + +public class TagReplacementOptions : IComparable +{ + public readonly string OldTag; + public readonly string NewTag; + public readonly int StartIndex; + + public TagReplacementOptions(string currentTag, string newTag, int startIndex) + { + NewTag = newTag ?? throw new ArgumentNullException(nameof(newTag)); + OldTag = currentTag ?? throw new ArgumentNullException(nameof(currentTag)); + + if (startIndex < 0) + throw new ArgumentOutOfRangeException(nameof(startIndex)); + + StartIndex = startIndex; + } + + public int CompareTo(object? obj) + { + if (obj is not TagReplacementOptions other) + throw new ArgumentException($"Object must be of type {nameof(TagReplacementOptions)}"); + + if (ReferenceEquals(this, other)) return 0; + return StartIndex.CompareTo(other.StartIndex); + } +} \ No newline at end of file diff --git a/cs/Markdown/TagReplacementSpecification.cs b/cs/Markdown/TagReplacementSpecification.cs new file mode 100644 index 000000000..843de1cd9 --- /dev/null +++ b/cs/Markdown/TagReplacementSpecification.cs @@ -0,0 +1,45 @@ +using System.Collections; + +namespace Markdown; + +public class TagReplacementSpecification +{ + public readonly string InputOpeningTag; + public readonly string InputClosingTag; + public readonly string OutputOpeningTag; + public readonly string OutputClosingTag; + + public readonly IEnumerable InvalidSubstringsInMarkup; + private readonly IEnumerable forbiddenPrefixesOfOpeningTag; + private readonly IEnumerable forbiddenPrefixesOfClosingTag; + + public TagReplacementSpecification(IEnumerable invalidSubstringsInMarkup, + string inputOpeningTag, string outputOpeningTag, + string inputClosingTag = null, string outputClosingTag = null, + IEnumerable forbiddenPrefixesOfOpeningTag = null, + IEnumerable forbiddenPrefixesOfClosingTag = null) + { + InputOpeningTag = inputOpeningTag ?? throw new ArgumentNullException(nameof(inputOpeningTag)); + InputClosingTag = inputClosingTag ?? inputOpeningTag; + OutputOpeningTag = outputOpeningTag ?? throw new ArgumentNullException(nameof(outputOpeningTag)); + OutputClosingTag = outputClosingTag ?? outputOpeningTag[0] + "/" + outputOpeningTag.Substring(1); + + if (invalidSubstringsInMarkup.Any(substring => substring.Length > inputOpeningTag.Length + 2)) + throw new ArgumentException("Запрещенные подстроки не могут быть длиннее inputOpeningTag.Length + 2"); + + InvalidSubstringsInMarkup = invalidSubstringsInMarkup; + this.forbiddenPrefixesOfClosingTag = forbiddenPrefixesOfClosingTag ?? []; + this.forbiddenPrefixesOfOpeningTag = forbiddenPrefixesOfOpeningTag ?? []; + } + + private bool CheckForbiddenPrefixes(string substring, IEnumerable forbiddenPrefixes) + { + if (forbiddenPrefixes.Any(substring.EndsWith)) + return false; + return substring[1] != '\\' || substring.StartsWith(@"\\"); + } + + public bool CheckOpeningTag(string substring) => CheckForbiddenPrefixes(substring, forbiddenPrefixesOfOpeningTag); + + public bool CheckClosingTag(string substring) => CheckForbiddenPrefixes(substring, forbiddenPrefixesOfClosingTag); +} \ No newline at end of file diff --git a/cs/Markdown/TextFragment.cs b/cs/Markdown/TextFragment.cs new file mode 100644 index 000000000..b1b67af37 --- /dev/null +++ b/cs/Markdown/TextFragment.cs @@ -0,0 +1,20 @@ +namespace Markdown; + +internal class TextFragment +{ + public readonly int StartIndex; + public readonly int Length; + public readonly TagReplacementSpecification Specification; + + public TextFragment(int startIndex, int length, TagReplacementSpecification specification) + { + if (startIndex < 0) + throw new ArgumentException($"{nameof(startIndex)} не может быть отрицательным"); + if (length < 0) + throw new ArgumentException($"{nameof(length)} не может быть отрицательной"); + + Length = length; + StartIndex = startIndex; + Specification = specification ?? throw new ArgumentNullException(nameof(specification)); + } +} diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..57fe23675 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -1,13 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{FF6D69A1-B013-4659-8FF2-2BD6C0026278}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,5 +29,15 @@ 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 + {FF6D69A1-B013-4659-8FF2-2BD6C0026278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF6D69A1-B013-4659-8FF2-2BD6C0026278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF6D69A1-B013-4659-8FF2-2BD6C0026278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF6D69A1-B013-4659-8FF2-2BD6C0026278}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {56A810F3-3750-4BA8-A365-FA046394FB15} 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