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 => $"{Name}>";
+
+ 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\nitalic\nheader
"
+ ).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