diff --git a/MarkdownSpec.md b/MarkdownSpec.md index 886e99c95..031420035 100644 --- a/MarkdownSpec.md +++ b/MarkdownSpec.md @@ -70,4 +70,8 @@ __Непарные_ символы в рамках одного абзаца н превратится в: -\

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

\ No newline at end of file +\

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

+ +# Маркированный список + +Абзацы начиная с "- ", выделяются тегом \
  • до конца абзаца. Внутри может быть другая разметка. Может быть обхвачен заголовком. \ No newline at end of file diff --git a/cs/Markdown/Data/Marks.cs b/cs/Markdown/Data/Marks.cs new file mode 100644 index 000000000..31dd73b52 --- /dev/null +++ b/cs/Markdown/Data/Marks.cs @@ -0,0 +1,41 @@ +namespace Markdown.Data; + +public static class Marks +{ + public const string Bold = "__"; + public const string Italic = "_"; + public const string Header = "#"; + public const string List = "-"; + + public static readonly IEnumerable AllMarks = new[] + { + Bold, + Italic, + Header, + List + }; + + public static string GetMarkByTagName(string name) + { + switch (name) + { + case TagNames.Header: + return Header; + case TagNames.Strong: + return Bold; + case TagNames.Em: + return Italic; + case TagNames.List: + return List; + default: + throw new ArgumentException("Wrong name!"); + } + } + + public static int AfterMarkSpace(string mark) + { + if (mark == Header || mark == List) + return 1; + return 0; + } +} \ No newline at end of file diff --git a/cs/Markdown/Data/PositionedTag.cs b/cs/Markdown/Data/PositionedTag.cs new file mode 100644 index 000000000..a8d9add2f --- /dev/null +++ b/cs/Markdown/Data/PositionedTag.cs @@ -0,0 +1,16 @@ +namespace Markdown.Data; + +public class PositionedTag : Tag +{ + public int Position { get; } + public bool IsOpenClose { get; } + public bool IsOpening { get; } + public string Mark => Marks.GetMarkByTagName(Name); + + public PositionedTag(int position, string mark, bool isOpening = true, bool isOpenClose = false) : base(TagFactory.BuildTag(mark).Name) + { + IsOpenClose = isOpenClose; + IsOpening = isOpening; + Position = position; + } +} \ No newline at end of file diff --git a/cs/Markdown/Data/Tag.cs b/cs/Markdown/Data/Tag.cs new file mode 100644 index 000000000..7c98f2664 --- /dev/null +++ b/cs/Markdown/Data/Tag.cs @@ -0,0 +1,13 @@ +namespace Markdown.Data; + +public class Tag +{ + public string Name { get; } + public string OpenTag => $"<{Name}>"; + public string CloseTag => $""; + + public Tag(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/cs/Markdown/Data/TagFactory.cs b/cs/Markdown/Data/TagFactory.cs new file mode 100644 index 000000000..adebff08f --- /dev/null +++ b/cs/Markdown/Data/TagFactory.cs @@ -0,0 +1,33 @@ +namespace Markdown.Data; + +public static class TagNames { + public const string Strong = "strong"; + public const string Em = "em"; + public const string Header = "h1"; + public const string List = "li"; +} + +public static class TagFactory +{ + private static Tag _bold => new(TagNames.Strong); + private static Tag _italic => new(TagNames.Em); + private static Tag _header => new(TagNames.Header); + private static Tag _list => new(TagNames.List); + + public static Tag BuildTag(string mark) + { + switch (mark) + { + case Marks.Header: + return _header; + case Marks.Bold: + return _bold; + case Marks.Italic: + return _italic; + case Marks.List: + return _list; + default: + throw new ArgumentException("Wrong mark!"); + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Data/Token.cs b/cs/Markdown/Data/Token.cs new file mode 100644 index 000000000..465ac7e67 --- /dev/null +++ b/cs/Markdown/Data/Token.cs @@ -0,0 +1,35 @@ +namespace Markdown.Data; + +public class Token +{ + private Tag _tag; + private string _mark => Marks.GetMarkByTagName(_tag.Name); + public string Head => _tag.OpenTag; + public string Tail => _tag.CloseTag; + public int StartPosition { get; } + public int EndPosition { get; } + + public int Gap(bool isOpened) + { + if (_mark == Marks.Header || _mark == Marks.List) + { + if (isOpened) + { + return _mark.Length + 1; + } + return 0; + } + return _mark.Length; + } + + public Token(Tag tag, int start, int end) + { + this._tag = tag; + StartPosition = start; + EndPosition = end; + } + + public Token(string mark, int start, int end) : this(TagFactory.BuildTag(mark), start, end) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..3cd22e17b --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,15 @@ + + + + net6 + enable + enable + + + + + + + + + diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..8cac7d5ec --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,54 @@ +using System.Text; +using Markdown.Data; + +namespace Markdown; + +public class Md +{ + public static string Render(string input) + { + var parser = new TokenParser(); + var tokens = parser.ParseTokens(input); + + return GenerateHtml(input, tokens); + } + + public static string GenerateHtml(string text, IEnumerable tokens) + { + var mergedText = new StringBuilder(); + + var tokenStack = new Stack(); + var prevPosition = 0; + var outerEnd = text.Length + 3; + foreach (var token in tokens) + { + if (token.StartPosition >= outerEnd) + { + while (tokenStack.Count > 0) + { + var tokenPrev = tokenStack.Pop(); + mergedText.Append(text.Substring(prevPosition,tokenPrev.EndPosition - prevPosition)); + mergedText.Append(tokenPrev.Tail); + prevPosition = tokenPrev.EndPosition + tokenPrev.Gap(false); + } + } + mergedText.Append(text.Substring(prevPosition,token.StartPosition-prevPosition)); + mergedText.Append(token.Head); + prevPosition = token.StartPosition + token.Gap(true); + outerEnd = token.EndPosition + token.Gap(false); + tokenStack.Push(token); + } + + while (tokenStack.Count > 0) + { + var token = tokenStack.Pop(); + mergedText.Append(text.Substring(prevPosition,token.EndPosition - prevPosition)); + mergedText.Append(token.Tail); + prevPosition = token.EndPosition + token.Gap(false); + } + mergedText.Append(text.Substring(prevPosition, text.Length - prevPosition)); + return mergedText.ToString(); + } +} + + diff --git a/cs/Markdown/ParserValidator.cs b/cs/Markdown/ParserValidator.cs new file mode 100644 index 000000000..51c6c4303 --- /dev/null +++ b/cs/Markdown/ParserValidator.cs @@ -0,0 +1,63 @@ +namespace Markdown; +using Markdown.Data; + +public class ParserValidator +{ + private string _text; + + public ParserValidator(string input) + { + _text = input; + } + + public bool IsMarkCorrect(int startIndex, bool isOpening, int markLength = 1) + { + var isScreened = IsScreened(startIndex); + if (isOpening) + { + return !isScreened + && startIndex + markLength < _text.Length + && !Char.IsWhiteSpace(_text[startIndex + markLength]); + } + return !isScreened + && startIndex > 0 + && !Char.IsWhiteSpace(_text[startIndex - 1]); + } + + public bool IsScreened(int index) + { + return (index > 0 && _text[index - 1] == '\\') + && (index > 1 && _text[index - 2] != '\\' || index == 1); + } + + public bool IsDoubleUnderscore(int index) + { + return index < _text.Length - 1 && _text[index + 1] == '_' + || index > 0 && _text[index - 1] == '_'; + } + + public bool IsContentAcceptable(string content, string mark) + { + return !string.IsNullOrEmpty(content) && (HasNoDigits(content) || mark == Marks.Header || mark == Marks.List); + } + + public bool IsSplittingWords(int start, int end) + { + return start > 0 && _text[start - 1] != ' ' + && end < _text.Length - 1 && _text[end + 1] != ' ' + && _text.Substring(start, end - start).Any(Char.IsWhiteSpace) + && _text[end] != '\n'; + } + + private bool HasNoDigits(string content) + { + foreach (char c in content) + { + if (char.IsDigit(c)) + return false; + } + return true; + } + + +} \ No newline at end of file diff --git a/cs/Markdown/Tests/Markdown_Tests.cs b/cs/Markdown/Tests/Markdown_Tests.cs new file mode 100644 index 000000000..1c556d42f --- /dev/null +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -0,0 +1,101 @@ +using System.Diagnostics; +using FluentAssertions; +using NUnit.Framework; +using System.Text; +using Markdown.Data; + +namespace Markdown.Tests; + +[TestFixture] +class Markdown_Tests +{ + public static IEnumerable GenerateHtmlSource() + { + yield return new TestCaseData( + "# main title\n__some bold text__", + "

    main title

    \nsome bold text", + new [] + { + new Token(Marks.Header, 0, 12), + new Token(Marks.Bold, 13, 29) + } + ).SetName("Simple text"); + yield return new TestCaseData( + "# main title\n__some _bold_ text__", + "

    main title

    \nsome bold text", + new [] + { + new Token(Marks.Header, 0, 12), + new Token(Marks.Bold, 13, 31), + new Token(Marks.Italic, 20, 25) + } + ).SetName("Token inside token"); + } + + [Test, TestCaseSource(nameof(GenerateHtmlSource))] + public void GenerateHtml_DifferentText(string actual, string expected, Token[] tokens) + { + Md.GenerateHtml(actual, tokens).Should().Be(expected); + } + + public static IEnumerable Render_Source() + { + yield return new TestCaseData( + "п# з __ж _к_ ж__ з\nп", + "п

    з ж к ж з

    \nп" + ).SetName("Small text for debugging"); + yield return new TestCaseData( + "# Заголовок __с _разными_ символами__", + "

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

    " + ).SetName("Simple text"); + yield return new TestCaseData( + "- __bold__\n- _italic_\n- # header", + "
  • bold
  • \n
  • italic
  • \n
  • header

  • " + ).SetName("Simple list"); + yield return new TestCaseData( + "# Заголовок с# заголовком\n\n _нач_ало се__реди__на ко_нец_ а __также _вложенные_ тэги __сам_ых__ раз_ных__ видов__", + "

    Заголовок с

    заголовком

    \n\n начало середина конец а __также вложенные тэги __сам_ых__ раз_ных__ видов__" + ).SetName("Normal text"); + yield return new TestCaseData( + "# Спецификация языка разметки\n\nПосмотрите этот файл в сыром виде. Сравните с тем, что показывает github.\nВсе совпадения случайны ;)\n\n\n\n# Курсив\n\nТекст, _окруженный с двух сторон_ одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\ вот так:\n\nТекст, \\окруженный с двух сторон\\ одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\.\n\n\n\n# Полужирный\n\n__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега \\.\n\n\n\n# Экранирование\n\nЛюбой символ можно экранировать, чтобы он не считался частью разметки.\n\\_Вот это\\_, не должно выделиться тегом \\.\n\nСимвол экранирования исчезает из результата, только если экранирует что-то.\nЗдесь сим\\волы экранирования\\ \\должны остаться.\\\n\nСимвол экранирования тоже можно экранировать: \\\\_вот это будет выделено тегом_ \\\n\n\n\n# Взаимодействие тегов\n\nВнутри __двойного выделения _одинарное_ тоже__ работает.\n\nНо не наоборот — внутри _одинарного __двойное__ не_ работает.\n\nПодчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка.\n\nОднако выделять часть слова они могут: и в _нач_але, и в сер_еди_не, и в кон_це._\n\nВ то же время выделение в ра_зных сл_овах не работает.\n\n__Непарные_ символы в рамках одного абзаца не считаются выделением.\n\nЗа подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением \nи остаются просто символами подчерка.\n\nПодчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки _не считаются_ окончанием выделения \nи остаются просто символами подчерка.\n\nВ случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением.\n\nЕсли внутри подчерков пустая строка ____, то они остаются символами подчерка.\n\n\n\n# Заголовки\n\nАбзац, начинающийся с '\\# ', выделяется тегом \\

    в заголовок.\nВ тексте заголовка могут присутствовать все прочие символы разметки с указанными правилами.\n\nТаким образом\n\n# Заголовок __с _разными_ символами__\n\nпревратится в:\n\n\\

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

    ", + "

    Спецификация языка разметки

    \n\nПосмотрите этот файл в сыром виде. Сравните с тем, что показывает github.\nВсе совпадения случайны ;)\n\n\n\n

    Курсив

    \n\nТекст, окруженный с двух сторон одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\ вот так:\n\nТекст, \\окруженный с двух сторон\\ одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\.\n\n\n\n

    Полужирный

    \n\n__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега \\.\n\n\n\n

    Экранирование

    \n\nЛюбой символ можно экранировать, чтобы он не считался частью разметки.\n\\_Вот это\\_, не должно выделиться тегом \\.\n\nСимвол экранирования исчезает из результата, только если экранирует что-то.\nЗдесь сим\\волы экранирования\\ \\должны остаться.\\\n\nСимвол экранирования тоже можно экранировать: \\\\_вот это будет выделено тегом_ \\\n\n\n\n

    Взаимодействие тегов

    \n\nВнутри двойного выделения одинарное тоже работает.\n\nНо не наоборот — внутри одинарного __двойное__ не работает.\n\nПодчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка.\n\nОднако выделять часть слова они могут: и в начале, и в середине, и в конце.\n\nВ то же время выделение в ра_зных сл_овах не работает.\n\n__Непарные_ символы в рамках одного абзаца не считаются выделением.\n\nЗа подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением \nи остаются просто символами подчерка.\n\nПодчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки не считаются окончанием выделения \nи остаются просто символами подчерка.\n\nВ случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением.\n\nЕсли внутри подчерков пустая строка ____, то они остаются символами подчерка.\n\n\n\n

    Заголовки

    \n\nАбзац, начинающийся с \'\\# \', выделяется тегом \\

    в заголовок.\nВ тексте заголовка могут присутствовать все прочие символы разметки с указанными правилами.\n\nТаким образом\n\n

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

    \n\nпревратится в:\n\n\\

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

    " + ).SetName("Biggest text"); + } + + [Test, TestCaseSource(nameof(Render_Source))] + public void Render_DifferentText(string actual, string expected) + { + Md.Render(actual).Should().Be(expected); + } + + [Test, Explicit] + [TestCase(10000, 10)] + [TestCase(10000, 1000)] + public void ЕfficiencyTest(long n, long coefficient) + { + var input = StackString(n); + var stopwatch = Stopwatch.StartNew(); + var res = Md.Render(input); + stopwatch.Stop(); + var time1 = stopwatch.ElapsedMilliseconds * coefficient; + Console.WriteLine("Normal text time: " + stopwatch.ElapsedMilliseconds); + input = StackString(n * coefficient); + stopwatch = Stopwatch.StartNew(); + res = Md.Render(input); + stopwatch.Stop(); + var time2 = stopwatch.ElapsedMilliseconds; + Console.WriteLine("Text * koef time: " + stopwatch.ElapsedMilliseconds); + time2.Should().BeLessThan(time1); + } + + private string StackString(long times) + { + var sb = new StringBuilder(); + for (int i = 0; i < times; i++) + { + sb.Append("__a"); + } + sb.Append("__"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tests/Tags_Tests.cs b/cs/Markdown/Tests/Tags_Tests.cs new file mode 100644 index 000000000..c5fbf0609 --- /dev/null +++ b/cs/Markdown/Tests/Tags_Tests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using NUnit.Framework; +using Markdown.Data; + +namespace Markdown.Tests; + +[TestFixture] +class Tags_Tests +{ + [Test] + public void Build_Recognize_OnWrongMark() + { + var act = () => TagFactory.BuildTag("1"); + act.Should().Throw(); + } + + [Test] + [ + TestCase("#", "h1"), + TestCase("_", "em"), + TestCase("__", "strong"), + TestCase("-", "li") + ] + public void Build_CorrectTag_OnCorrectMark(string mark, string correctName) + { + var tag = TagFactory.BuildTag(mark); + var correctTag = new Tag(correctName); + + tag.Should().BeEquivalentTo(correctTag); + } + + [Test] + public void OpenCloseOutput_IsCorrect() + { + var tag = TagFactory.BuildTag("#"); + var text = tag.OpenTag + "text" + tag.CloseTag; + + text.Should().Be("

    text

    "); + } +} \ No newline at end of file diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs new file mode 100644 index 000000000..514e2c172 --- /dev/null +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -0,0 +1,182 @@ +using FluentAssertions; +using NUnit.Framework; +using Markdown.Data; + +namespace Markdown.Tests; + +[TestFixture] +class TokenParser_Tests +{ + private void ParseText(string actualInput, Token[] expectedTokens) + { + var parser = new TokenParser(); + var actualTokens = parser.ParseTokens(actualInput); + var act = () => Md.GenerateHtml(actualInput, actualTokens); + var actualHtml = Md.GenerateHtml(actualInput, actualTokens); + var expectedHtml = Md.GenerateHtml(actualInput, expectedTokens); + + act.Should().NotThrow(); + actualHtml.Should().Be(expectedHtml); + actualTokens.Should().BeEquivalentTo(expectedTokens); + } + + #region basic tests + public static IEnumerable ParseSimpleText_Source() + { + yield return new TestCaseData( + "__main title__\n__some bold text__", + new Token[] + { + new (Marks.Bold, 0, 12), + new (Marks.Bold, 15, 31) + }).SetName("Bold text"); + yield return new TestCaseData( + "_main title_\n_some italic text_", + new Token[] + { + new (Marks.Italic, 0, 11), + new (Marks.Italic, 13, 30) + }).SetName("Italic text"); + yield return new TestCaseData( + "# main title\n# some header text", + new Token[] + { + new (Marks.Header, 0, 12), + new (Marks.Header, 13, 31) + }).SetName("Headers text"); + yield return new TestCaseData( + "- main title\n- some bold text", + new Token[] + { + new (Marks.List, 0, 12), + new (Marks.List, 13, 29) + }).SetName("List text"); + yield return new TestCaseData( + "- __bold__\n- _italic_\n- # header", + new Token[] + { + new (Marks.List, 0, 10), + new (Marks.Bold, 2, 8), + new (Marks.List, 11, 21), + new (Marks.Italic, 13, 20), + new (Marks.List, 22, 32), + new (Marks.Header, 24, 32) + } + ).SetName("List with tags inside"); + yield return new TestCaseData( + "# - 123\n", + new Token[] + { + new (Marks.Header, 0, 7), + new (Marks.List, 2, 7), + } + ).SetName("List inside header"); + } + + [Test, TestCaseSource(nameof(ParseSimpleText_Source))] + public void ParseText_OnSimpleText(string actualInput, Token[] expectedTokens) + { + ParseText(actualInput, expectedTokens); + } + + public static IEnumerable ParseNestingText_Source() + { + yield return new TestCaseData( + "# __main title__\n__some bold text__", + new Token[] + { + new (Marks.Header, 0, 16), + new (Marks.Bold, 2, 14), + new (Marks.Bold, 17, 33) + }).SetName("Inside header"); + yield return new TestCaseData( + "# __main title__\n# __some _bold_ text__", + new Token[] + { + new (Marks.Header, 0, 16), + new (Marks.Bold, 2, 14), + new (Marks.Header, 17, 39), + new (Marks.Bold, 19, 37), + new (Marks.Italic, 26, 31) + }).SetName("Italic inside Bold"); + } + + [Test, TestCaseSource(nameof(ParseNestingText_Source))] + public void ParseText_OnNestingText(string actualInput, Token[] expectedTokens) + { + ParseText(actualInput, expectedTokens); + } + # endregion + + public static IEnumerable ParseTextWithExceptions_Source() + { + yield return new TestCaseData( + "внутри _одинарного __двойное__ не_ работает", + new Token[] + { + new (Marks.Italic, 7, 33) + }).SetName("Bold inside Italic"); + yield return new TestCaseData( + "c цифрами_12_3 не считаются выделением __даже1так__", + new Token[] { } + ).SetName("Numbers aren't tagged"); + yield return new TestCaseData( + "_нач_ало се_ред_ина ко_нец_", + new Token[] + { + new (Marks.Italic, 0, 4), + new (Marks.Italic, 11, 15), + new (Marks.Italic, 22, 26), + } + ).SetName("Parts of words"); + yield return new TestCaseData( + "эти__ подчерки__ не считаются и эти __подчерки __не считаются", + new Token[] { } + ).SetName("Spaces after/before bold mark"); + yield return new TestCaseData( + "эти_ подчерки_ не считаются и эти _подчерки _не считаются", + new Token[] { } + ).SetName("Spaces after/before italic mark"); + yield return new TestCaseData( + "выделение в ра_зных сл_овах н__е работ__ает", + new Token[] { } + ).SetName("Words splitted"); + yield return new TestCaseData( + "__Непарные символы в рамках одного абзаца не считаются выделением", + new Token[] { } + ).SetName("Use only pairs of marks"); + yield return new TestCaseData( + "пустая строка ____", + new Token[] { } + ).SetName("Empty text inside tags"); + yield return new TestCaseData( + "__пересечения _двойных__ и одинарных_", + new Token[] { } + ).SetName("Crossing marks from example"); + yield return new TestCaseData( + "__пересечения _двойных__ и __одинарных_ подчерков__", + new Token[] + { + new (Marks.Bold, 0, 49), + new (Marks.Italic, 14, 38) + } + ).SetName("Crossing marks"); + yield return new TestCaseData( + "_пересечения __двойных_ и _одинарных__ подчерков_", + new Token[] { } + ).SetName("Crossing marks reversed"); + yield return new TestCaseData( + @"экран\_ирование\_ и \\__двойное\\__ экрани\рование", + new Token[] + { + new (Marks.Bold,22, 33) + } + ).SetName("Shielding marks"); + } + + [Test, TestCaseSource(nameof(ParseTextWithExceptions_Source))] + public void ParseText_WithExceptions(string actualInput, Token[] expectedTokens) + { + ParseText(actualInput, expectedTokens); + } +} \ No newline at end of file diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs new file mode 100644 index 000000000..d3c0dc461 --- /dev/null +++ b/cs/Markdown/TokenParser.cs @@ -0,0 +1,226 @@ +using Markdown.Data; + +namespace Markdown; + +public class TokenParser +{ + private List _tokens; + private string _text; + private ParserValidator _validator; + private Stack _stackOfTags; + + public TokenParser(string text = "") + { + _text = text; + _validator = new (text); + _tokens = new (); + _stackOfTags = new (); + } + + public IEnumerable ParseTokens(string input, string outerTokenMark = "") + { + _text = input; + _validator = new ParserValidator(input); + _tokens = new List(); + _stackOfTags = new Stack(); + + _tokens.AddRange(FindAllTokens()); + + _tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); + return _tokens; + } + + private IEnumerable FindAllTokens() + { + for (int i = 0; i < _text.Length; i++) + { + if (_text[i] == '\n') + { + foreach (var token in BuildTokensFromStack(i)) + { + yield return token; + } + continue; + } + + var findedTag = FindTag(i); + if (findedTag == null) + continue; + + if (_stackOfTags.Count > 0) + { + var lastTag = _stackOfTags.Pop(); + if (lastTag.Name == findedTag.Name) + { + if (CheckOuterToken()) + continue; + + if (lastTag.IsOpening && (!findedTag.IsOpening || findedTag.IsOpenClose)) + { + var token = BuildTokenOrNull(lastTag, findedTag); + if (token is not null) + yield return token; + } + else + { + if (lastTag.Name == TagNames.Header || lastTag.Name == TagNames.List) + _stackOfTags.Push(lastTag); + PushIfOpened(findedTag); + } + } + else + { + + _stackOfTags.Push(lastTag); + _stackOfTags.Push(findedTag); + } + } + else + PushIfOpened(findedTag); + } + foreach (var token in BuildTokensFromStack(_text.Length)) + { + yield return token; + } + } + + private bool CheckOuterToken() + { + if (_stackOfTags.Count == 0) + return false; + var outerTag = _stackOfTags.Pop(); + _stackOfTags.Push(outerTag); + if (outerTag.Name == TagNames.Em) + { + return true; + } + return false; + } + + private IEnumerable BuildTokensFromStack(int lineEnd) + { + while (_stackOfTags.Count > 0) + { + var current = _stackOfTags.Pop(); + if (current.Mark == Marks.Header || current.Mark == Marks.List) + { + var token = BuildTokenOrNull(current, new PositionedTag(lineEnd, current.Mark, false)); + if (token is not null) + yield return token; + } + } + } + + private Token? BuildTokenOrNull(PositionedTag startTag, PositionedTag endTag) + { + var mark = startTag.Mark; + var start = startTag.Position; + var end = endTag.Position; + + if (end - start < 2 || startTag.Name != endTag.Name) + return null; + + var content = _text.Substring(start + mark.Length + Marks.AfterMarkSpace(mark), endTag.Position - start - mark.Length - Marks.AfterMarkSpace(mark)); + + if (!_validator.IsContentAcceptable(content, mark) || _validator.IsSplittingWords(start, end + mark.Length)) + { + return null; + } + + var token = new Token( + TagFactory.BuildTag(mark), + start, + end + ); + return token; + } + + private void PushIfOpened(PositionedTag findedTag) + { + if (findedTag.IsOpening) + _stackOfTags.Push(findedTag); + } + + private PositionedTag? FindTag(int index) + { + foreach (var mark in Marks.AllMarks) + { + var tag = GetPositionedTagOrNull(index, mark); + if (tag is not null) + return tag; + } + + return null; + } + + #region GetPositionedTag + + private PositionedTag? GetPositionedTagOrNull(int index, string mark) + { + var isOpening = CheckByMark(index, mark, true); + var isClosing = CheckByMark(index, mark, false); + if (isOpening && isClosing) + { + return new PositionedTag(index, mark, isOpenClose:true); + } + if (isOpening) + return new PositionedTag(index, mark, isOpening:isOpening); + if (isClosing) + return new PositionedTag(index, mark, isOpening:isOpening); + + return null; + } + + private bool CheckByMark(int index, string mark, bool isOpening) + { + switch (mark) + { + case Marks.Bold: + return CheckMarkForBold(index, isOpening); + case Marks.Italic: + return CheckMarkForItalic(index, isOpening); + case Marks.Header: + return CheckMarkForHeader(index, isOpening); + case Marks.List: + return CheckMarkForList(index, isOpening); + default: + return false; + } + } + + private bool CheckMarkForBold(int index, bool isOpening) + { + return index + 1 < _text.Length + && _text[index] == '_' + && _text[index + 1] == '_' + && _validator.IsMarkCorrect(index, isOpening, Marks.Bold.Length); + } + + private bool CheckMarkForItalic(int index, bool isOpening) + { + return index < _text.Length + && _text[index] == '_' + && !_validator.IsDoubleUnderscore(index) + && _validator.IsMarkCorrect(index, isOpening, Marks.Italic.Length); + } + + private bool CheckMarkForHeader(int index, bool isOpening) + { + return isOpening + && index + 1 < _text.Length + && _text[index].ToString() == Marks.Header + && char.IsWhiteSpace(_text[index + 1]) + && !_validator.IsScreened(index); + } + + private bool CheckMarkForList(int index, bool isOpening) + { + return isOpening + && index + 1 < _text.Length + && _text[index].ToString() == Marks.List + && char.IsWhiteSpace(_text[index+1]) + && !_validator.IsScreened(index); + } + + #endregion +} \ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..7cf19158b 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{0AD6F565-D2EB-4101-834A-55957466656A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,5 +29,17 @@ 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 + {0AD6F565-D2EB-4101-834A-55957466656A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AD6F565-D2EB-4101-834A-55957466656A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AD6F565-D2EB-4101-834A-55957466656A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AD6F565-D2EB-4101-834A-55957466656A}.Release|Any CPU.Build.0 = Release|Any CPU + {CF721144-6FE9-4481-9F28-CE94039819A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF721144-6FE9-4481-9F28-CE94039819A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF721144-6FE9-4481-9F28-CE94039819A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF721144-6FE9-4481-9F28-CE94039819A9}.Release|Any CPU.Build.0 = Release|Any CPU + {FFC3C1D5-B071-4055-B1B2-4CC67576280F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFC3C1D5-B071-4055-B1B2-4CC67576280F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFC3C1D5-B071-4055-B1B2-4CC67576280F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFC3C1D5-B071-4055-B1B2-4CC67576280F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/cs/clean-code.sln.DotSettings b/cs/clean-code.sln.DotSettings index 135b83ecb..53fe49b2f 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" WarnAboutPrefixesAndSuffixes="False" 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" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016