diff --git a/cs/Markdown/BinaryTrees.cs b/cs/Markdown/BinaryTrees.cs new file mode 100644 index 000000000..ddbf170d5 --- /dev/null +++ b/cs/Markdown/BinaryTrees.cs @@ -0,0 +1,120 @@ +using System.Collections; + +namespace Markdown; + +public class BinaryTree : IEnumerable + where T : IComparable +{ + private TreeNode? root; + + public void Add(T key) + { + var currentSubtree = root; + if (Equals(root, null)) + { + root = new TreeNode(key, null); + return; + } + + while (true) + if (currentSubtree != null && key.CompareTo(currentSubtree.Value) >= 0) + { + currentSubtree.HeightOfRight++; + if (currentSubtree.Right == null) + { + currentSubtree.Right = new TreeNode(key, currentSubtree); + return; + } + currentSubtree = currentSubtree.Right; + } + else + { + if (currentSubtree == null) continue; + currentSubtree.HeightOfLeft++; + if (currentSubtree.Left == null) + { + currentSubtree.Left = new TreeNode(key, currentSubtree); + return; + } + + currentSubtree = currentSubtree.Left; + } + } + + public T this[int i] + { + get + { + if (root != null && (root.HeightOfRight + root.HeightOfLeft < i || i < 0)) + throw new IndexOutOfRangeException(); + + var currentSubtree = root; + var index = 0; + + while (true) + { + if (currentSubtree != null && currentSubtree.HeightOfLeft + index == i) + return currentSubtree.Value; + if (currentSubtree != null && currentSubtree.HeightOfLeft + index > i) + currentSubtree = currentSubtree.Left; + else if (currentSubtree != null && 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(T value, TreeNode? ancestor) : IEnumerable + { + public readonly T Value = value; + public int HeightOfLeft { get; set; } + public int HeightOfRight { get; set; } + + public TreeNode? Left, Right; + private readonly TreeNode? ancestor = ancestor; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + var treeNode = this; + + while (!Equals(treeNode.Left, null)) + treeNode = treeNode.Left; + + while (true) + { + if (treeNode == null) continue; + 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..2150e3797 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/cs/Markdown/MarkupSpecification/ISpecificationProvider.cs b/cs/Markdown/MarkupSpecification/ISpecificationProvider.cs new file mode 100644 index 000000000..3a37d2009 --- /dev/null +++ b/cs/Markdown/MarkupSpecification/ISpecificationProvider.cs @@ -0,0 +1,9 @@ +using Markdown.Tags; +using Markdown.Tags.TagSpecification; + +namespace Markdown.MarkupSpecification; + +public interface ISpecificationProvider +{ + public IEnumerable GetMarkupSpecification(); +} \ No newline at end of file diff --git a/cs/Markdown/MarkupSpecification/MdToHtmlSpecificationBuilder.cs b/cs/Markdown/MarkupSpecification/MdToHtmlSpecificationBuilder.cs new file mode 100644 index 000000000..5d47216ab --- /dev/null +++ b/cs/Markdown/MarkupSpecification/MdToHtmlSpecificationBuilder.cs @@ -0,0 +1,18 @@ +using Markdown.Tags; +using Markdown.Tags.TagSpecification; + +namespace Markdown.MarkupSpecification; + +public class MdToHtmlSpecificationBuilder : ISpecificationProvider +{ + public IEnumerable GetMarkupSpecification() + { + return + [ + new BoldTag(), + new HeaderTag(), + new ItalicsTag(), + new BulletedListTag() + ]; + } +} \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..d206a4bdf --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,117 @@ +using System.Text; +using Markdown.MarkupSpecification; +using Markdown.Tags; +using Markdown.Tags.TagSpecification; + +namespace Markdown; + +public class Md(ISpecificationProvider specificationProvider) +{ + public string Render(string markdown) + { + var markupSpecification = specificationProvider.GetMarkupSpecification().ToArray(); + + var tagReplacements = FindAllTags(markdown, markupSpecification); + var renderedString = PerformTextFormatting(markdown, tagReplacements.ToArray()); + + return RemoveEscapingOfControlSubstrings(renderedString, markupSpecification); + } + + private IEnumerable FindAllTags(string text, BaseTag[] markupSpecification) + { + var result = new BinaryTree(); + + foreach (var tagSpecification in markupSpecification) + { + var fragment = tagSpecification.FindNextPairOfTags(text, 0, tagSpecification); + + while (fragment is not null) + { + if (fragment.OpeningTag.StartIndex > fragment.ClosingTag.StartIndex) + throw new Exception("Открывающийся тег должен находиться перед закрывающимся"); + + result.Add(fragment.OpeningTag); + result.Add(fragment.ClosingTag); + + var nextStartIndex = fragment.ClosingTag.StartIndex + fragment.ClosingTag.Tag.Old.Length; + if (nextStartIndex >= text.Length) break; + + fragment = tagSpecification.FindNextPairOfTags(text, nextStartIndex, tagSpecification); + } + } + + return EliminateTagConflictsAndIntersections(result); + } + + private BinaryTree EliminateTagConflictsAndIntersections( + BinaryTree tags) + { + var stack = new Stack(); + TagReplacementSpecification? openingTag = null; + var binaryTree = new BinaryTree(); + var len = tags.Count(); + + for (var i = 0; i < len; i++) + { + if (openingTag is not null && openingTag.Markup.Closing == tags[i].Tag) + { + binaryTree.Add(openingTag); + binaryTree.Add(tags[i]); + openingTag = stack.Count != 0 ? stack.Pop() : null; + } + else if (tags[i].Tag == tags[i].Markup.Opening) + { + if ((openingTag is null || !openingTag.Markup.DidConflict(tags[i].Markup)) && + !stack.Any(specification => specification.Markup.DidConflict(tags[i].Markup))) + { + if (openingTag is not null) stack.Push(openingTag); + openingTag = tags[i]; + } + } + if (stack.Any(specification => specification.Markup == tags[i].Markup)) + { + openingTag = stack.Pop(); + while(openingTag.Markup != tags[i].Markup) + openingTag = stack.Pop(); + } + } + + return binaryTree; + } + + private string PerformTextFormatting(string text, TagReplacementSpecification[] replacements) + { + if (replacements.Length == 0) + return text; + + var result = new StringBuilder(); + var endOfLastReplacement = -1; + + for (var i = 0; i < replacements.Length; i++) + { + result.Append(text[(endOfLastReplacement + 1)..replacements[i].StartIndex]); + result.Append(replacements[i].Markup.PerformTagFormatting( + replacements[i], + i != 0 ? replacements[i - 1].Markup : null, + i < replacements.Length - 1 ? replacements[i + 1].Markup : null)); + endOfLastReplacement = replacements[i].StartIndex + replacements[i].Tag.Old.Length - 1; + } + + if (endOfLastReplacement + 1 != text.Length) + result.Append(text[(endOfLastReplacement + 1)..text.Length]); + + return result.ToString(); + } + + private string RemoveEscapingOfControlSubstrings(string text, IEnumerable tags) + { + foreach (var tag in tags) + { + text = text.Replace('\\' + tag.Opening.Old, tag.Opening.Old); + if (tag.Closing.Old != tag.Opening.Old) + text = text.Replace('\\' + tag.Closing.Old, tag.Closing.Old); + } + + return text.Replace(@"\\", "\\"); + } +} \ No newline at end of file diff --git a/cs/Markdown/Program.cs b/cs/Markdown/Program.cs new file mode 100644 index 000000000..cee00b87f --- /dev/null +++ b/cs/Markdown/Program.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public class Program +{ + public static void Main() + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tags/Tag.cs b/cs/Markdown/Tags/Tag.cs new file mode 100644 index 000000000..b8b464d1d --- /dev/null +++ b/cs/Markdown/Tags/Tag.cs @@ -0,0 +1,4 @@ +namespace Markdown.Tags; + + +public record Tag(string Old, string New); \ No newline at end of file diff --git a/cs/Markdown/Tags/TagReplacementSpecification.cs b/cs/Markdown/Tags/TagReplacementSpecification.cs new file mode 100644 index 000000000..3365e0108 --- /dev/null +++ b/cs/Markdown/Tags/TagReplacementSpecification.cs @@ -0,0 +1,13 @@ +using Markdown.Tags.TagSpecification; + +namespace Markdown.Tags; + +public record TagReplacementSpecification(Tag Tag, BaseTag Markup, int StartIndex) : IComparable +{ + public int CompareTo(object? obj) + { + if (obj is TagReplacementSpecification specification) + return StartIndex.CompareTo(specification.StartIndex); + throw new ArgumentException($"Object must be of type {nameof(TagReplacementSpecification)}"); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tags/TagSpecification/BaseTag.cs b/cs/Markdown/Tags/TagSpecification/BaseTag.cs new file mode 100644 index 000000000..8598274c5 --- /dev/null +++ b/cs/Markdown/Tags/TagSpecification/BaseTag.cs @@ -0,0 +1,50 @@ +namespace Markdown.Tags.TagSpecification; + +public abstract class BaseTag(Tag opening, Tag closing) +{ + public Tag Opening { get; } = opening; + public Tag Closing { get; } = closing; + + public virtual bool DidConflict(BaseTag tag) => false; + + public virtual TextFragment? FindNextPairOfTags(string text, int startIndex, BaseTag tagSpecification) + { + var opening = FindNextTag(text, startIndex, Opening, tagSpecification); + if (opening is null) return null; + + startIndex = opening.StartIndex + Opening.Old.Length; + var closing = FindNextTag(text, startIndex, Closing, tagSpecification); + + if (closing is null) return null; + if (startIndex == closing.StartIndex) + return FindNextPairOfTags(text, closing.StartIndex + closing.Tag.Old.Length, tagSpecification); + + return new TextFragment(opening, closing); + } + + protected virtual TagReplacementSpecification? FindNextTag(string text, int startIndex, Tag tag, BaseTag tagSpecification) + { + var numberOfEscapeCharacters = 0; + + for (var i = startIndex; i <= text.Length - tag.Old.Length; i++) + { + if (text[i] == '\\') numberOfEscapeCharacters++; + else if (numberOfEscapeCharacters % 2 == 0 && text.Substring(i, tag.Old.Length) == tag.Old && + AdditionallyCheckCurrentPosition(text, i, tag)) + { + return new TagReplacementSpecification(tag, tagSpecification, i); + } + else numberOfEscapeCharacters = 0; + } + + return null; + } + + public virtual string PerformTagFormatting(TagReplacementSpecification replacement, + BaseTag? previousTag, BaseTag? nextTag) + { + return replacement.Tag == Opening ? Opening.New : Closing.New; + } + + protected virtual bool AdditionallyCheckCurrentPosition(string text, int currentIndex, Tag tag) => true; +} \ No newline at end of file diff --git a/cs/Markdown/Tags/TagSpecification/BasicSingleTag.cs b/cs/Markdown/Tags/TagSpecification/BasicSingleTag.cs new file mode 100644 index 000000000..dfab184bf --- /dev/null +++ b/cs/Markdown/Tags/TagSpecification/BasicSingleTag.cs @@ -0,0 +1,14 @@ +namespace Markdown.Tags.TagSpecification; + +public abstract class BasicSingleTag(Tag opening, Tag closing) : BaseTag(opening, closing) +{ + protected override bool AdditionallyCheckCurrentPosition(string text, int currentIndex, Tag tag) + { + var startIndex = Math.Max(currentIndex - Environment.NewLine.Length, 0); + + if (tag == Opening) + return currentIndex == 0 || text[startIndex..currentIndex] == Environment.NewLine; + + return true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tags/TagSpecification/BoldTag.cs b/cs/Markdown/Tags/TagSpecification/BoldTag.cs new file mode 100644 index 000000000..011ad75bb --- /dev/null +++ b/cs/Markdown/Tags/TagSpecification/BoldTag.cs @@ -0,0 +1,3 @@ +namespace Markdown.Tags.TagSpecification; + +public class BoldTag() : BaseTag(new Tag("__", ""), new Tag("__", "")); \ No newline at end of file diff --git a/cs/Markdown/Tags/TagSpecification/BulletedListTag.cs b/cs/Markdown/Tags/TagSpecification/BulletedListTag.cs new file mode 100644 index 000000000..6178d48c2 --- /dev/null +++ b/cs/Markdown/Tags/TagSpecification/BulletedListTag.cs @@ -0,0 +1,13 @@ +namespace Markdown.Tags.TagSpecification; + +public class BulletedListTag() : BasicSingleTag(new Tag("- ", "
  • "), new Tag(Environment.NewLine, "
  • ")) +{ + public override string PerformTagFormatting(TagReplacementSpecification replacement, + BaseTag? previousTag, BaseTag? nextTag) + { + if (replacement.Tag == Opening) + return previousTag is BulletedListTag ? replacement.Tag.New : "
      " + replacement.Tag.New; + + return nextTag is BulletedListTag ? replacement.Tag.New : replacement.Tag.New + "
    "; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tags/TagSpecification/HeaderTag.cs b/cs/Markdown/Tags/TagSpecification/HeaderTag.cs new file mode 100644 index 000000000..8de649d3b --- /dev/null +++ b/cs/Markdown/Tags/TagSpecification/HeaderTag.cs @@ -0,0 +1,3 @@ +namespace Markdown.Tags.TagSpecification; + +public class HeaderTag() : BasicSingleTag(new Tag("# ", "

    "), new Tag(Environment.NewLine, "

    ")); \ No newline at end of file diff --git a/cs/Markdown/Tags/TagSpecification/ItalicsTag.cs b/cs/Markdown/Tags/TagSpecification/ItalicsTag.cs new file mode 100644 index 000000000..cb5b79dcc --- /dev/null +++ b/cs/Markdown/Tags/TagSpecification/ItalicsTag.cs @@ -0,0 +1,56 @@ +namespace Markdown.Tags.TagSpecification; + +public class ItalicsTag : BaseTag +{ + private readonly string[] forbiddenSubstrings; + + public ItalicsTag() : base(new Tag("_", ""), new Tag("_", "")) + { + var list = new List { "__" }; + + for (var i = 0; i < 10; i++) + { + list.Add($"_{i}"); + list.Add($"{i}_"); + } + + forbiddenSubstrings = list.ToArray(); + } + + public override bool DidConflict(BaseTag tag) => tag is BoldTag; + + public override TextFragment? FindNextPairOfTags(string text, int startIndex, BaseTag tagSpecification) + { + var pair = base.FindNextPairOfTags(text, startIndex, tagSpecification); + + if (pair is null) + return null; + if (CheckLocationOfTagsInText(text, pair)) + return pair; + + return FindNextPairOfTags(text, pair.ClosingTag.StartIndex, tagSpecification); + } + + protected override bool AdditionallyCheckCurrentPosition(string text, int currentIndex, Tag tag) + { + var index = Math.Max(currentIndex - 1, 0); + var substring = text.Substring(index, Math.Min(currentIndex + tag.Old.Length, text.Length - 1) - index + 1); + + return !forbiddenSubstrings.Any(s => substring.StartsWith(s) || substring.EndsWith(s)) && + (tag == Opening && substring[^1] != ' ' || tag == Closing && substring[0] != ' '); + } + + private bool CheckLocationOfTagsInText(string text, TextFragment pairOfTags) + { + var indexBeforeFragment = pairOfTags.OpeningTag.StartIndex - 1; + var indexAfterFragment = pairOfTags.ClosingTag.StartIndex + pairOfTags.ClosingTag.Tag.Old.Length; + var fragment = text[pairOfTags.OpeningTag.StartIndex..(pairOfTags.ClosingTag.StartIndex + Closing.Old.Length)]; + var tagOpensBeforeWord = indexBeforeFragment >= 0 && + (char.IsWhiteSpace(text, indexBeforeFragment) || text[indexBeforeFragment] == '\\') || + pairOfTags.OpeningTag.StartIndex == 0; + var tagClosesAfterWord = indexAfterFragment < text.Length && char.IsWhiteSpace(text, indexAfterFragment) || + indexAfterFragment == text.Length; + + return fragment.Split().Length == 1 || tagOpensBeforeWord && tagClosesAfterWord; + } +} \ No newline at end of file diff --git a/cs/Markdown/TextFragment.cs b/cs/Markdown/TextFragment.cs new file mode 100644 index 000000000..68f1e6a1b --- /dev/null +++ b/cs/Markdown/TextFragment.cs @@ -0,0 +1,5 @@ +using Markdown.Tags; + +namespace Markdown; + +public record TextFragment(TagReplacementSpecification OpeningTag, TagReplacementSpecification ClosingTag); \ No newline at end of file diff --git a/cs/MarkdownTests/HtmlText.html b/cs/MarkdownTests/HtmlText.html new file mode 100644 index 000000000..1dcbe526c --- /dev/null +++ b/cs/MarkdownTests/HtmlText.html @@ -0,0 +1 @@ +

    Вот несколько простых правил, которые помогут вам оставаться здоровыми:

    • Правильное питание. Старайтесь есть больше овощей, фруктов, злаков и белков. Избегайте жирной, солёной и сладкой пищи. Помните, что здоровое питание — это не только полезно, но и вкусно!
    • Физическая активность. Регулярные занятия спортом помогут вам укрепить мышцы, улучшить работу сердечно-сосудистой системы и повысить настроение. Выберите то, что вам нравится: бег, плавание, йога или танцы. Главное — двигаться!
    • Здоровый сон. Качественный сон необходим для восстановления сил и здоровья. Старайтесь ложиться и вставать в одно и то же время, создайте комфортные условия для сна и избегайте экранов гаджетов перед сном.
    • Отказ от вредных привычек. Курение, алкоголь и другие вредные привычки могут серьёзно подорвать здоровье. Постарайтесь отказаться от них или хотя бы ограничить потребление.
    • Регулярные медицинские осмотры. Не забывайте посещать врача для профилактических осмотров и своевременного выявления возможных проблем со здоровьем.
    \ No newline at end of file diff --git a/cs/MarkdownTests/MarkdownTests.csproj b/cs/MarkdownTests/MarkdownTests.csproj new file mode 100644 index 000000000..7b0f4e169 --- /dev/null +++ b/cs/MarkdownTests/MarkdownTests.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/cs/MarkdownTests/MarkdownText.txt b/cs/MarkdownTests/MarkdownText.txt new file mode 100644 index 000000000..5faef38ea --- /dev/null +++ b/cs/MarkdownTests/MarkdownText.txt @@ -0,0 +1,6 @@ +# Вот несколько простых правил, которые помогут вам оставаться здоровыми: +- __Правильное питание.__ Старайтесь есть больше овощей, фруктов, злаков и белков. Избегайте жирной, солёной и сладкой пищи. _Помните, что здоровое питание — это не только полезно, но и вкусно!_ +- __Физическая активность.__ Регулярные занятия спортом помогут вам укрепить мышцы, улучшить работу сердечно-сосудистой системы и повысить настроение. _Выберите то, что вам нравится: бег, плавание, йога или танцы. Главное — двигаться!_ +- __Здоровый сон.__ Качественный сон необходим для восстановления сил и здоровья. Старайтесь ложиться и вставать в одно и то же время, создайте комфортные условия для сна и избегайте экранов гаджетов перед сном. +- __Отказ от вредных привычек.__ _Курение, алкоголь и другие вредные привычки могут серьёзно подорвать здоровье._ Постарайтесь отказаться от них или хотя бы ограничить потребление. +- __Регулярные медицинские осмотры.__ Не забывайте посещать врача для профилактических осмотров и своевременного выявления возможных проблем со здоровьем. diff --git a/cs/MarkdownTests/MdTests.cs b/cs/MarkdownTests/MdTests.cs new file mode 100644 index 000000000..4f9b85c8e --- /dev/null +++ b/cs/MarkdownTests/MdTests.cs @@ -0,0 +1,176 @@ +using System.Diagnostics; +using System.Text; +using FluentAssertions; +using Markdown; +using Markdown.MarkupSpecification; + +namespace MarkdownTests; + + +[TestFixture] +internal class MdTests +{ + private Md md; + + [SetUp] + public void InitializeFild() + { + md = new Md(new MdToHtmlSpecificationBuilder()); + } + + [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); + } + + [Test] + public void Render_Heading_TurnsIntoTagH1() + { + var actual = md.Render($"# Заголовок{Environment.NewLine} текст"); + + actual.Should().Be("

    Заголовок

    текст"); + } + + [Test] + public void Render_HeadingWithDifferentKeyCharacters() + { + var actual = md.Render($"# Заголовок __с _разными_ символами__{Environment.NewLine}"); + + actual.Should().Be("

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

    "); + } + + [Test] + public void Render_OneBulletedListItem_WrappedInHtmlTag() + { + var actual = md.Render($"- Элемент маркированного списка{Environment.NewLine}"); + + actual.Should().Be("
    • Элемент маркированного списка
    "); + } + + [Test] + public void Render_MultipleBulletedListItems_CombinedIntoCommonULTag() + { + var actual = md.Render($"- Первый элемент{Environment.NewLine}" + + $"- Второй элемент{Environment.NewLine}" + + $"- Третий элемент{Environment.NewLine}"); + + actual.Should().Be("
    • Первый элемент
    • Второй элемент
    • Третий элемент
    "); + } + + [Test] + public void Render_MarkdownText_HtmlText() + { + var mdText = File.ReadAllText("MarkdownText.txt"); + var expected = File.ReadAllText("HtmlText.html"); + + var actual = md.Render(mdText); + + actual.Should().Be(expected); + } + + [Test] + public void Render_AlgorithmShouldBeLinear() + { + var repetitionCount = 100; + var shortMdText = File.ReadAllText("MarkdownText.txt"); + var bigMdText = PerformTextConcatenation(shortMdText, 20); + Action action = text => { md.Render(text); }; + + var timeForShortText = MeasureDurationInMs(shortMdText, action, repetitionCount); + var timeForBigText = MeasureDurationInMs(bigMdText, action, repetitionCount); + var timeRatio = timeForBigText / timeForShortText; + + timeRatio.Should().BeLessThan(2.0 * bigMdText.Length / shortMdText.Length); + } + + private string PerformTextConcatenation(string text, int repetitionCount) + { + var builder = new StringBuilder(); + + for (var i = 0; i < repetitionCount; i++) + builder.Append(text); + + return builder.ToString(); + } + + private double MeasureDurationInMs(string text, Action action, int repetitionCount) + { + action(text); + GC.Collect(); + GC.WaitForPendingFinalizers(); + var watch = new Stopwatch(); + + watch.Restart(); + + for (var i = 0; i < repetitionCount; i++) + action(text); + + watch.Stop(); + + return watch.Elapsed.TotalMilliseconds / repetitionCount; + } +} diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..ad97e6019 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.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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownTests", "MarkdownTests\MarkdownTests.csproj", "{BAB03885-96F8-4806-8222-353293EC8BF0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,5 +31,21 @@ 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 + {BAB03885-96F8-4806-8222-353293EC8BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB03885-96F8-4806-8222-353293EC8BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB03885-96F8-4806-8222-353293EC8BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB03885-96F8-4806-8222-353293EC8BF0}.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 + GlobalSection(NestedProjects) = preSolution 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