From a0c10e00b5b2c75d26e3ea80dc882037ab9f08af Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Sat, 1 Nov 2025 22:21:11 +0500 Subject: [PATCH 01/12] Project structure --- cs/Markdown/Markdown.csproj | 9 +++++++++ cs/Markdown/Markdown_Tests.cs | 8 ++++++++ cs/Markdown/Md.cs | 21 +++++++++++++++++++++ cs/Markdown/Tag.cs | 15 +++++++++++++++ cs/Markdown/TagFactory.cs | 14 ++++++++++++++ cs/Markdown/Token.cs | 28 ++++++++++++++++++++++++++++ cs/Markdown/Tokeniezer.cs | 21 +++++++++++++++++++++ 7 files changed, 116 insertions(+) create mode 100644 cs/Markdown/Markdown.csproj create mode 100644 cs/Markdown/Markdown_Tests.cs create mode 100644 cs/Markdown/Md.cs create mode 100644 cs/Markdown/Tag.cs create mode 100644 cs/Markdown/TagFactory.cs create mode 100644 cs/Markdown/Token.cs create mode 100644 cs/Markdown/Tokeniezer.cs diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/cs/Markdown/Markdown_Tests.cs b/cs/Markdown/Markdown_Tests.cs new file mode 100644 index 000000000..636e87141 --- /dev/null +++ b/cs/Markdown/Markdown_Tests.cs @@ -0,0 +1,8 @@ +namespace Markdown +{ + [TextFixture] + class Markdown_Tests + { + + } +} \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..bd10198de --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,21 @@ +namespace Markdown +{ + public class Md + { + // Моя идея заключается в том, что найдя все действующие "inline elements" + // сохранить позиции их содержимого в Token-ы чтобы при сборке html + // поочередно вставлять в StringBuilder html-тэги из токенов и исходный текст. + + public static string Render(string input) + { + throw new Exception(); + } + + private static string GenerateHtml(string text, List tokens) + { + throw new Exception(); + } + } +} + + diff --git a/cs/Markdown/Tag.cs b/cs/Markdown/Tag.cs new file mode 100644 index 000000000..0bba5d333 --- /dev/null +++ b/cs/Markdown/Tag.cs @@ -0,0 +1,15 @@ +namespace Markdown +{ + public class Tag + { + public string Name { get; } + public string OpenTag => $"<{Name}>"; + public string CloseTag => $""; + + public Tag(string name) + { + + } + + } +} \ No newline at end of file diff --git a/cs/Markdown/TagFactory.cs b/cs/Markdown/TagFactory.cs new file mode 100644 index 000000000..063322d90 --- /dev/null +++ b/cs/Markdown/TagFactory.cs @@ -0,0 +1,14 @@ +namespace Markdown +{ + public static class TagFactory + { + private static Tag Bold => new("strong"); + private static Tag Italic => new("em"); + private static Tag Title => new("h1"); + + public static Tag BuildTag(string mark) + { + throw new Exception(); + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs new file mode 100644 index 000000000..04d025059 --- /dev/null +++ b/cs/Markdown/Token.cs @@ -0,0 +1,28 @@ +namespace Markdown +{ + public class Token + { + private string value; + private Tag tag; + public string Head => tag.OpenTag; + public string Tail => tag.CloseTag; + public int position; + public int Length { get { return value.Length; } } + + + public Token(Tag tag, string value) + { + + } + + public Token(string mark, string value) + { + + } + + private Tag RecognizeMark(string mark) + { + throw new Exception(); + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokeniezer.cs b/cs/Markdown/Tokeniezer.cs new file mode 100644 index 000000000..86790fbbd --- /dev/null +++ b/cs/Markdown/Tokeniezer.cs @@ -0,0 +1,21 @@ +namespace Markdown +{ + public class Tokeniezer + { + public Token[] GetTokens(string text) + { + throw new Exception(); + } + + private IEnumerable ParseText(string text) + { + throw new Exception(); + } + + private Token[] TokeniezeLine(string line) + { + throw new Exception(); + } + } +} + From 78dc1e13d3c8fe29257632527ab2ea9f60e4059f Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Sat, 1 Nov 2025 22:25:44 +0500 Subject: [PATCH 02/12] Tokenizer is static and renamed --- cs/Markdown/Tokeniezer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cs/Markdown/Tokeniezer.cs b/cs/Markdown/Tokeniezer.cs index 86790fbbd..5adaf6b66 100644 --- a/cs/Markdown/Tokeniezer.cs +++ b/cs/Markdown/Tokeniezer.cs @@ -1,18 +1,18 @@ namespace Markdown { - public class Tokeniezer - { - public Token[] GetTokens(string text) + public static class Tokeniezer + { + public static Token[] GetTokens(string text) { throw new Exception(); } - private IEnumerable ParseText(string text) + private static IEnumerable ParseText(string text) { throw new Exception(); } - private Token[] TokeniezeLine(string line) + private static Token[] TokeniezeLine(string line) { throw new Exception(); } From 4684df84a6b1374a2712ef9739d22ec3563b3174 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Fri, 7 Nov 2025 17:36:55 +0500 Subject: [PATCH 03/12] base Parser and tests --- cs/Markdown/Markdown.csproj | 10 +- cs/Markdown/Markdown_Tests.cs | 131 +++++++++++++++++++++++++- cs/Markdown/Md.cs | 27 ++++-- cs/Markdown/Tag.cs | 7 +- cs/Markdown/TagFactory.cs | 24 ++++- cs/Markdown/Token.cs | 33 +++++-- cs/Markdown/TokenParser.cs | 172 ++++++++++++++++++++++++++++++++++ cs/Markdown/Tokeniezer.cs | 21 ----- cs/clean-code.sln | 14 +++ cs/clean-code.sln.DotSettings | 3 + 10 files changed, 394 insertions(+), 48 deletions(-) create mode 100644 cs/Markdown/TokenParser.cs delete mode 100644 cs/Markdown/Tokeniezer.cs diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj index fa71b7ae6..3cd22e17b 100644 --- a/cs/Markdown/Markdown.csproj +++ b/cs/Markdown/Markdown.csproj @@ -1,9 +1,15 @@ - + - net8.0 + net6 enable enable + + + + + + diff --git a/cs/Markdown/Markdown_Tests.cs b/cs/Markdown/Markdown_Tests.cs index 636e87141..b4f15c7ef 100644 --- a/cs/Markdown/Markdown_Tests.cs +++ b/cs/Markdown/Markdown_Tests.cs @@ -1,8 +1,137 @@ +using FluentAssertions; +using NUnit.Framework; + namespace Markdown { - [TextFixture] + [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("#", "main title", 0, 12), + new Token("__", "some bold text", 13, 31) + } + ).SetName("Simple text"); + yield return new TestCaseData( + "# main title\n__some _bold_ text__", + "

main title

\nsome bold text", + new [] + { + new Token("#", "main title", 0, 12), + new Token("__", "some bold text", 13, 33) + } + ).SetName("Token inside token"); + } + [Test, TestCaseSource(nameof(GenerateHtmlSource))] + public void GenerateHtml_DifferentText(string actual, string expected, Token[] tokens) + { + var t = new TokenParser().ParseTokens(actual); + Md.GenerateHtml(actual, tokens).Should().Be(expected); + } + } + + [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); + + act.Should().NotThrow(); + var lookUp = Md.GenerateHtml(actualInput, actualTokens); + var lookUp2 = Md.GenerateHtml(actualInput, expectedTokens); + actualTokens.Should().BeEquivalentTo(expectedTokens); + } + + public static IEnumerable ParseSimpleText_Source() + { + yield return new TestCaseData( + "__main title__\n__some bold text__", + new [] + { + new Token("__", "main title", 0, 14), + new Token("__", "some bold text", 15, 33) + }).SetName("Bold text"); + yield return new TestCaseData( + "_main title_\n_some italic text_", + new [] + { + new Token("_", "main title", 0, 12), + new Token("_", "some italic text", 13, 31) + }).SetName("Italic text"); + yield return new TestCaseData( + "# main title\n# some header text", + new [] + { + new Token("#", "main title", 0, 12), + new Token("#", "some header text", 13, 31) + }).SetName("Headers text"); + } + + [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 [] + { + new Token("#", "main title", 0, 16), + new Token("__", "some bold text", 15, 33) + }).SetName("Inside header"); + } + + [Test, TestCaseSource(nameof(ParseNestingText_Source))] + public void ParseText_OnNestingText(string actualInput, Token[] expectedTokens) + { + ParseText(actualInput, expectedTokens); + } + } + + [TestFixture] + class Tags_Tests + { + [Test] + public void Build_Recognize_OnWrongMark() + { + var act = () => HtmlTagFactory.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 = HtmlTagFactory.BuildTag(mark); + var correctTag = new Tag(correctName); + + tag.Should().BeEquivalentTo(correctTag); + } + + [Test] + public void OpenCloseOutput_IsCorrect() + { + var tag = HtmlTagFactory.BuildTag("#"); + var text = tag.OpenTag + "text" + tag.CloseTag; + + text.Should().Be("

text

"); + } } } \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index bd10198de..b272c39d3 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -1,19 +1,34 @@ -namespace Markdown +using System.Text; + +namespace Markdown { public class Md { // Моя идея заключается в том, что найдя все действующие "inline elements" // сохранить позиции их содержимого в Token-ы чтобы при сборке html // поочередно вставлять в StringBuilder html-тэги из токенов и исходный текст. - + public static string Render(string input) { - throw new Exception(); + var parser = new TokenParser(); + var tokens = parser.ParseTokens(input); + + return GenerateHtml(input, tokens); } - - private static string GenerateHtml(string text, List tokens) + + public static string GenerateHtml(string text, IEnumerable tokens) { - throw new Exception(); + var mergedText = new StringBuilder(); + var prevPosition = 0; + foreach (var token in tokens) + { + var currentPosition = token.StartPosition; + mergedText.Append(text.Substring(prevPosition,currentPosition-prevPosition)); + mergedText.Append(token.ToString()); + prevPosition = token.EndPosition; + } + mergedText.Append(text.Substring(prevPosition)); + return mergedText.ToString(); } } } diff --git a/cs/Markdown/Tag.cs b/cs/Markdown/Tag.cs index 0bba5d333..c4bbc5935 100644 --- a/cs/Markdown/Tag.cs +++ b/cs/Markdown/Tag.cs @@ -1,15 +1,16 @@ namespace Markdown { + public class Tag { - public string Name { get; } + private string name; + public string Name { get { return name; } } public string OpenTag => $"<{Name}>"; public string CloseTag => $""; public Tag(string name) { - + this.name = name; } - } } \ No newline at end of file diff --git a/cs/Markdown/TagFactory.cs b/cs/Markdown/TagFactory.cs index 063322d90..4e793a474 100644 --- a/cs/Markdown/TagFactory.cs +++ b/cs/Markdown/TagFactory.cs @@ -1,14 +1,28 @@ namespace Markdown { - public static class TagFactory + public static class HtmlTagFactory { - private static Tag Bold => new("strong"); - private static Tag Italic => new("em"); - private static Tag Title => new("h1"); + public static Tag Bold => new("strong"); + public static Tag Italic => new("em"); + public static Tag Title => new("h1"); + public static Tag MarkedList => new("li"); + public static Tag BuildTag(string mark) { - throw new Exception(); + switch (mark) + { + case "#": + return Title; + case "__": + return Bold; + case "_": + return Italic; + case "*": + return MarkedList; + default: + throw new Exception("Wrong mark!"); + } } } } \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 04d025059..3779ae23e 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -1,28 +1,41 @@ +using System.Text; +using NUnit.Framework; + namespace Markdown { public class Token { private string value; private Tag tag; + private int start; + private int end; public string Head => tag.OpenTag; public string Tail => tag.CloseTag; - public int position; - public int Length { get { return value.Length; } } - + public int Length => value.Length; + + public int StartPosition => start; + public int EndPosition => end; + public int OriginalLength => EndPosition - StartPosition; - public Token(Tag tag, string value) + public Token(Tag tag, string value, int start, int end) { - + this.tag = tag; + this.value = value; + this.start = start; + this.end = end; } - public Token(string mark, string value) + public Token(string mark, string value, int start, int end) : this(HtmlTagFactory.BuildTag(mark), value, start, end) { - } - - private Tag RecognizeMark(string mark) + + public override string ToString() { - throw new Exception(); + var builder = new StringBuilder(); + builder.Append(Head); + builder.Append(value); + builder.Append(Tail); + return builder.ToString(); } } } \ No newline at end of file diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs new file mode 100644 index 000000000..2449d8497 --- /dev/null +++ b/cs/Markdown/TokenParser.cs @@ -0,0 +1,172 @@ +namespace Markdown +{ + public class TokenParser + { + private List tokens = new(); + + public IEnumerable ParseTokens(string input) + { + tokens = new List(); + tokens.AddRange(FindBoldTokens(input)); + tokens.AddRange(FindItalicTokens(input)); + tokens.AddRange(FindHeaderTokens(input)); + + tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); + return tokens; + } + + private IEnumerable FindBoldTokens(string input) + { + var stack = new Stack(); // Храним позиции открывающих тегов + + for (var i = 0; i < input.Length - 1; i++) + { + // Проверяем два символа подряд + if (input[i] == '_' && input[i + 1] == '_') + { + if (stack.Count > 0) + { + // Нашли закрывающий тег + var start = stack.Pop(); + var end = i + 2; // +2 потому что два символа + + // Извлекаем содержимое (без __ и __) + var content = input.Substring(start + 2, i - start - 2); + var htmlContent = Md.GenerateHtml(content, new TokenParser().FindItalicTokens(content)); + yield return new Token( + HtmlTagFactory.Bold, + htmlContent, + start, + end + ); + i++; // Пропускаем второй символ + } + else + { + // Нашли открывающий тег + stack.Push(i); + i++; // Пропускаем второй символ + } + } + } + } + + private IEnumerable FindItalicTokens(string input) + { + var stack = new Stack(); // Храним позиции открывающих тегов + + for (int i = 0; i < input.Length; i++) + { + if (input[i] == '_') + { + // Проверяем, что это не часть двойного подчеркивания + bool isDoubleUnderscore = i < input.Length - 1 && input[i + 1] == '_'; + + if (!isDoubleUnderscore) + { + if (stack.Count > 0) + { + // Нашли закрывающий тег + int start = stack.Pop(); + int end = i + 1; + + // Извлекаем содержимое (без _ и _) + string content = input.Substring(start + 1, i - start - 1); + + // Проверяем, что этот тег не пересекается с уже найденными + if (!IsOverlappingWithExisting(start, end)) + { + yield return new Token( + HtmlTagFactory.Italic, + content, + start, + end + ); + } + } + else + { + // Нашли открывающий тег + stack.Push(i); + } + } + } + } + } + + private bool IsOverlappingWithExisting(int start, int end) + { + foreach (var token in tokens) + { + if (start < token.EndPosition && end > token.StartPosition) + { + return true; + } + } + return false; + } + + private IEnumerable FindHeaderTokens(string input) + { + // Обрабатываем текст построчно для заголовков + int lineStart = 0; + + for (int i = 0; i < input.Length; i++) + { + if (input[i] == '\n' || i == input.Length - 1) + { + // Определяем конец строки + int lineEnd = (i == input.Length - 1) ? i + 1 : i; + int lineLength = lineEnd - lineStart; + + if (lineLength > 0) + { + foreach (var token in ProcessHeaderLine(input, lineStart, lineEnd)) + { + yield return token; + } + } + + lineStart = i + 1; // Начало следующей строки + } + } + + // Обрабатываем последнюю строку, если текст не заканчивается \n + if (lineStart < input.Length) + { + foreach (var token in ProcessHeaderLine(input, lineStart, input.Length)) + { + yield return token; + } + } + } + + private IEnumerable ProcessHeaderLine(string input, int lineStart, int lineEnd) + { + // Если есть # и пробел + int pos = lineStart; + while (pos < lineEnd && input[pos] != '#' && !char.IsWhiteSpace(input[pos+1])) + { + pos++; + } + + if (pos < lineEnd && input[pos] == '#') + { + // Пропускаем пробел после # + pos++; + + // Извлекаем содержимое заголовка + string content = input.Substring(pos, lineEnd - pos).Trim(); + var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content)); + + yield return new Token( + HtmlTagFactory.Title, + htmlContent, + pos - 1, + lineEnd + ); + } + } + } +} + diff --git a/cs/Markdown/Tokeniezer.cs b/cs/Markdown/Tokeniezer.cs deleted file mode 100644 index 5adaf6b66..000000000 --- a/cs/Markdown/Tokeniezer.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Markdown -{ - public static class Tokeniezer - { - public static Token[] GetTokens(string text) - { - throw new Exception(); - } - - private static IEnumerable ParseText(string text) - { - throw new Exception(); - } - - private static Token[] TokeniezeLine(string line) - { - throw new Exception(); - } - } -} - 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 From 1a124cc88e9ee733b76393c6f3a5f0637a86e564 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Sat, 8 Nov 2025 02:57:11 +0500 Subject: [PATCH 04/12] Parser update and new Validator and Tests --- cs/Markdown/Markdown_Tests.cs | 137 ---------------------- cs/Markdown/Md.cs | 6 + cs/Markdown/ParserValidator.cs | 59 ++++++++++ cs/Markdown/Tests/Markdown_Tests.cs | 37 ++++++ cs/Markdown/Tests/Tags_Tests.cs | 39 +++++++ cs/Markdown/Tests/TokenParser_Tests.cs | 134 ++++++++++++++++++++++ cs/Markdown/TokenParser.cs | 153 +++++++++++++++++-------- 7 files changed, 378 insertions(+), 187 deletions(-) delete mode 100644 cs/Markdown/Markdown_Tests.cs create mode 100644 cs/Markdown/ParserValidator.cs create mode 100644 cs/Markdown/Tests/Markdown_Tests.cs create mode 100644 cs/Markdown/Tests/Tags_Tests.cs create mode 100644 cs/Markdown/Tests/TokenParser_Tests.cs diff --git a/cs/Markdown/Markdown_Tests.cs b/cs/Markdown/Markdown_Tests.cs deleted file mode 100644 index b4f15c7ef..000000000 --- a/cs/Markdown/Markdown_Tests.cs +++ /dev/null @@ -1,137 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; - -namespace Markdown -{ - [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("#", "main title", 0, 12), - new Token("__", "some bold text", 13, 31) - } - ).SetName("Simple text"); - yield return new TestCaseData( - "# main title\n__some _bold_ text__", - "

main title

\nsome bold text", - new [] - { - new Token("#", "main title", 0, 12), - new Token("__", "some bold text", 13, 33) - } - ).SetName("Token inside token"); - } - - [Test, TestCaseSource(nameof(GenerateHtmlSource))] - public void GenerateHtml_DifferentText(string actual, string expected, Token[] tokens) - { - var t = new TokenParser().ParseTokens(actual); - Md.GenerateHtml(actual, tokens).Should().Be(expected); - } - } - - [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); - - act.Should().NotThrow(); - var lookUp = Md.GenerateHtml(actualInput, actualTokens); - var lookUp2 = Md.GenerateHtml(actualInput, expectedTokens); - actualTokens.Should().BeEquivalentTo(expectedTokens); - } - - public static IEnumerable ParseSimpleText_Source() - { - yield return new TestCaseData( - "__main title__\n__some bold text__", - new [] - { - new Token("__", "main title", 0, 14), - new Token("__", "some bold text", 15, 33) - }).SetName("Bold text"); - yield return new TestCaseData( - "_main title_\n_some italic text_", - new [] - { - new Token("_", "main title", 0, 12), - new Token("_", "some italic text", 13, 31) - }).SetName("Italic text"); - yield return new TestCaseData( - "# main title\n# some header text", - new [] - { - new Token("#", "main title", 0, 12), - new Token("#", "some header text", 13, 31) - }).SetName("Headers text"); - } - - [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 [] - { - new Token("#", "main title", 0, 16), - new Token("__", "some bold text", 15, 33) - }).SetName("Inside header"); - } - - [Test, TestCaseSource(nameof(ParseNestingText_Source))] - public void ParseText_OnNestingText(string actualInput, Token[] expectedTokens) - { - ParseText(actualInput, expectedTokens); - } - } - - [TestFixture] - class Tags_Tests - { - [Test] - public void Build_Recognize_OnWrongMark() - { - var act = () => HtmlTagFactory.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 = HtmlTagFactory.BuildTag(mark); - var correctTag = new Tag(correctName); - - tag.Should().BeEquivalentTo(correctTag); - } - - [Test] - public void OpenCloseOutput_IsCorrect() - { - var tag = HtmlTagFactory.BuildTag("#"); - var text = tag.OpenTag + "text" + tag.CloseTag; - - text.Should().Be("

text

"); - } - } -} \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index b272c39d3..90b61b500 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -20,12 +20,18 @@ public static string GenerateHtml(string text, IEnumerable tokens) { var mergedText = new StringBuilder(); var prevPosition = 0; + var nextPositionMin = -1; foreach (var token in tokens) { var currentPosition = token.StartPosition; + if (currentPosition < nextPositionMin) + { + continue; + } mergedText.Append(text.Substring(prevPosition,currentPosition-prevPosition)); mergedText.Append(token.ToString()); prevPosition = token.EndPosition; + nextPositionMin = token.EndPosition; } mergedText.Append(text.Substring(prevPosition)); return mergedText.ToString(); diff --git a/cs/Markdown/ParserValidator.cs b/cs/Markdown/ParserValidator.cs new file mode 100644 index 000000000..77d15b1a0 --- /dev/null +++ b/cs/Markdown/ParserValidator.cs @@ -0,0 +1,59 @@ +namespace Markdown; + +public class ParserValidator +{ + private string text; + + public ParserValidator(string input) + { + text = input; + } + + public bool IsMarkCorrect(int startIndex, bool isOpening, int markLength = 1) + { + var isScreened = (startIndex > 0 && text[startIndex - 1] == '\\') + && (startIndex > 1 && text[startIndex - 2] != '\\' || startIndex == 1); + if (isOpening) + { + return !isScreened + && startIndex + markLength < text.Length + && text[startIndex + markLength] != ' '; + } + else + { + return !isScreened + && startIndex > 0 + && text[startIndex - 1] != ' '; + } + } + + public bool IsDoubleUnderscore(int index) + { + return index < text.Length - 1 && text[index + 1] == '_' + || index > 0 && text[index - 1] == '_'; + } + + public bool IsContentAcceptable(string content) + { + return !string.IsNullOrEmpty(content) && HasNoDigits(content); + } + + 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).Contains(' '); + } + + public 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..8db1b243b --- /dev/null +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -0,0 +1,37 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown; + +[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("#", "main title", 0, 12), + new Token("__", "some bold text", 13, 31) + } + ).SetName("Simple text"); + yield return new TestCaseData( + "# main title\n__some _bold_ text__", + "

main title

\nsome bold text", + new [] + { + new Token("#", "main title", 0, 12), + new Token("__", "some bold text", 13, 33) + } + ).SetName("Token inside token"); + } + + [Test, TestCaseSource(nameof(GenerateHtmlSource))] + public void GenerateHtml_DifferentText(string actual, string expected, Token[] tokens) + { + var t = new TokenParser().ParseTokens(actual); + Md.GenerateHtml(actual, tokens).Should().Be(expected); + } +} \ 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..2105238e4 --- /dev/null +++ b/cs/Markdown/Tests/Tags_Tests.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown; + +[TestFixture] +class Tags_Tests +{ + [Test] + public void Build_Recognize_OnWrongMark() + { + var act = () => HtmlTagFactory.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 = HtmlTagFactory.BuildTag(mark); + var correctTag = new Tag(correctName); + + tag.Should().BeEquivalentTo(correctTag); + } + + [Test] + public void OpenCloseOutput_IsCorrect() + { + var tag = HtmlTagFactory.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..2f120712a --- /dev/null +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -0,0 +1,134 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown; + +[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); + + act.Should().NotThrow(); + var actualHtml = Md.GenerateHtml(actualInput, actualTokens); + var expectedHtml = Md.GenerateHtml(actualInput, expectedTokens); + expectedHtml.Should().Be(actualHtml); + actualTokens.Should().BeEquivalentTo(expectedTokens); + } + + #region basic tests + public static IEnumerable ParseSimpleText_Source() + { + yield return new TestCaseData( + "__main title__\n__some bold text__", + new [] + { + new Token("__", "main title", 0, 14), + new Token("__", "some bold text", 15, 33) + }).SetName("Bold text"); + yield return new TestCaseData( + "_main title_\n_some italic text_", + new [] + { + new Token("_", "main title", 0, 12), + new Token("_", "some italic text", 13, 31) + }).SetName("Italic text"); + yield return new TestCaseData( + "# main title\n# some header text", + new [] + { + new Token("#", "main title", 0, 12), + new Token("#", "some header text", 13, 31) + }).SetName("Headers text"); + } + + [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 [] + { + new Token("#", "main title", 0, 16), + new Token("__", "main title", 2, 16), + new Token("__", "some bold text", 17, 35) + }).SetName("Inside header"); + yield return new TestCaseData( + "# __main title__\n# __some _bold_ text__", + new [] + { + new Token("#", "main title", 0, 16), + new Token("__", "main title", 2, 16), + new Token("#", "some bold text", 17, 39), + new Token("__", "some bold text", 19, 39), + new Token("_", "bold", 26, 32) + }).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 [] + { + new Token("_", "одинарного __двойное__ не", 7, 34), + new Token("__", "двойное", 19, 30) + }).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[] { } + ).SetName("Spaces after/before 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"); + yield return new TestCaseData( + "_пересечения __двойных_ и _одинарных__ подчерков_", + new Token[] { } + ).SetName("Crossing marks reversed"); + yield return new TestCaseData( + @"экран\_ирование\_ и \\__двойное\\__ экрани\рование", + new Token[] + { + new Token("__", @"двойное\\", 22, 35) + } + ).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 index 2449d8497..f339da3a3 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -2,27 +2,43 @@ namespace Markdown { public class TokenParser { - private List tokens = new(); + private List tokens; + private List allFindedTokens; + private string text; + private ParserValidator validator; + + public TokenParser(string text = "") + { + this.text = text; + validator = new ParserValidator(text); + tokens = new List(); + allFindedTokens = new List(); + } public IEnumerable ParseTokens(string input) { + this.text = input; + validator = new ParserValidator(input); tokens = new List(); - tokens.AddRange(FindBoldTokens(input)); - tokens.AddRange(FindItalicTokens(input)); - tokens.AddRange(FindHeaderTokens(input)); + allFindedTokens = new List(); + + tokens.AddRange(FindBoldTokens()); + tokens.AddRange(FindItalicTokens()); + tokens.AddRange(FindHeaderTokens()); tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); return tokens; } - - private IEnumerable FindBoldTokens(string input) + + private IEnumerable FindBoldTokens() { var stack = new Stack(); // Храним позиции открывающих тегов - for (var i = 0; i < input.Length - 1; i++) + for (var i = 0; i < text.Length - 1; i++) { + var isOpeningTag = stack.Count == 0; // Проверяем два символа подряд - if (input[i] == '_' && input[i + 1] == '_') + if (text[i] == '_' && validator.IsDoubleUnderscore(i) && validator.IsMarkCorrect(i, isOpeningTag, 2)) { if (stack.Count > 0) { @@ -30,9 +46,17 @@ private IEnumerable FindBoldTokens(string input) var start = stack.Pop(); var end = i + 2; // +2 потому что два символа - // Извлекаем содержимое (без __ и __) - var content = input.Substring(start + 2, i - start - 2); - var htmlContent = Md.GenerateHtml(content, new TokenParser().FindItalicTokens(content)); + // Извлекаем содержимое ввиде текста и сразу делаем из него html + var content = text.Substring(start + 2, i - start - 2); + // Проверка на необходимость выделения + if (!validator.IsContentAcceptable(content) || validator.IsSplittingWords(start, end)) + { + continue; + } + + // Вписывание вложенных тэгов + var htmlContent = Md.GenerateHtml(content, new TokenParser(content).FindItalicTokens()); + yield return new Token( HtmlTagFactory.Bold, htmlContent, @@ -51,18 +75,19 @@ private IEnumerable FindBoldTokens(string input) } } - private IEnumerable FindItalicTokens(string input) + private IEnumerable FindItalicTokens() { + // Использовал тэг для возможных закрывающих марок var stack = new Stack(); // Храним позиции открывающих тегов - for (int i = 0; i < input.Length; i++) + for (int i = 0; i < text.Length; i++) { - if (input[i] == '_') + var isOpeningTag = stack.Count == 0; + + if (text[i] == '_' && validator.IsMarkCorrect(i, isOpeningTag)) { // Проверяем, что это не часть двойного подчеркивания - bool isDoubleUnderscore = i < input.Length - 1 && input[i + 1] == '_'; - - if (!isDoubleUnderscore) + if (!validator.IsDoubleUnderscore(i)) { if (stack.Count > 0) { @@ -70,18 +95,29 @@ private IEnumerable FindItalicTokens(string input) int start = stack.Pop(); int end = i + 1; - // Извлекаем содержимое (без _ и _) - string content = input.Substring(start + 1, i - start - 1); - + // Извлекаем содержимое (без "марок") + string content = text.Substring(start + 1, i - start - 1); + + // Проверка на необходимость выделения + if (!validator.IsContentAcceptable(content) || validator.IsSplittingWords(start, end)) + { + continue; + } + + var token = new Token( + HtmlTagFactory.Italic, + content, + start, + end + ); // Проверяем, что этот тег не пересекается с уже найденными - if (!IsOverlappingWithExisting(start, end)) + if (!CheckOverlappingWithExistingTokens(start, end)) { - yield return new Token( - HtmlTagFactory.Italic, - content, - start, - end - ); + yield return token; + } + else + { + allFindedTokens.Add(token); } } else @@ -94,34 +130,22 @@ private IEnumerable FindItalicTokens(string input) } } - private bool IsOverlappingWithExisting(int start, int end) - { - foreach (var token in tokens) - { - if (start < token.EndPosition && end > token.StartPosition) - { - return true; - } - } - return false; - } - - private IEnumerable FindHeaderTokens(string input) + private IEnumerable FindHeaderTokens() { // Обрабатываем текст построчно для заголовков int lineStart = 0; - for (int i = 0; i < input.Length; i++) + for (int i = 0; i < text.Length; i++) { - if (input[i] == '\n' || i == input.Length - 1) + if (text[i] == '\n' || i == text.Length - 1) { // Определяем конец строки - int lineEnd = (i == input.Length - 1) ? i + 1 : i; + int lineEnd = (i == text.Length - 1) ? i + 1 : i; int lineLength = lineEnd - lineStart; if (lineLength > 0) { - foreach (var token in ProcessHeaderLine(input, lineStart, lineEnd)) + foreach (var token in ProcessHeaderLine(text, lineStart, lineEnd)) { yield return token; } @@ -132,15 +156,14 @@ private IEnumerable FindHeaderTokens(string input) } // Обрабатываем последнюю строку, если текст не заканчивается \n - if (lineStart < input.Length) + if (lineStart < text.Length) { - foreach (var token in ProcessHeaderLine(input, lineStart, input.Length)) + foreach (var token in ProcessHeaderLine(text, lineStart, text.Length)) { yield return token; } } } - private IEnumerable ProcessHeaderLine(string input, int lineStart, int lineEnd) { // Если есть # и пробел @@ -157,7 +180,7 @@ private IEnumerable ProcessHeaderLine(string input, int lineStart, int li // Извлекаем содержимое заголовка string content = input.Substring(pos, lineEnd - pos).Trim(); - var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content)); + var htmlContent = Md.GenerateHtml(content, new TokenParser(text).ParseTokens(content)); yield return new Token( HtmlTagFactory.Title, @@ -167,6 +190,36 @@ private IEnumerable ProcessHeaderLine(string input, int lineStart, int li ); } } - } -} + + private bool CheckOverlappingWithExistingTokens(int start, int end) + { + var isOverlapping = false; + var toDelete = new List(); + allFindedTokens.AddRange(tokens); + foreach (var token in allFindedTokens) + { + if ( + end > token.EndPosition + && token.StartPosition < start && start < token.EndPosition + || token.StartPosition < end && end < token.EndPosition + && start < token.StartPosition + ) + { + // Удаляем токены пересекаемые найденным + toDelete.Add(token); + isOverlapping = true; + } + } + ClearOverlappingToken(toDelete); + return isOverlapping; + } + private void ClearOverlappingToken(IEnumerable toDelete) + { + foreach (var tokenToRemove in toDelete) + { + tokens.Remove(tokenToRemove); + } + } + } +} \ No newline at end of file From 61f1d38ce6d85604f4a043fc4815e26c04849444 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Sat, 8 Nov 2025 16:34:14 +0500 Subject: [PATCH 05/12] Full functional --- MarkdownSpec.md | 6 +- cs/Markdown/Marks.cs | 9 + cs/Markdown/Md.cs | 1 + cs/Markdown/ParserValidator.cs | 24 +-- cs/Markdown/TagFactory.cs | 16 +- cs/Markdown/Tests/Markdown_Tests.cs | 35 +++- cs/Markdown/Tests/Tags_Tests.cs | 2 +- cs/Markdown/Tests/TokenParser_Tests.cs | 62 ++++-- cs/Markdown/TokenParser.cs | 251 ++++++++++++------------- 9 files changed, 228 insertions(+), 178 deletions(-) create mode 100644 cs/Markdown/Marks.cs 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/Marks.cs b/cs/Markdown/Marks.cs new file mode 100644 index 000000000..21971411c --- /dev/null +++ b/cs/Markdown/Marks.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +public static class Marks +{ + public const string Bold = "__"; + public const string Italic = "_"; + public const string Header = "#"; + public const string List = "-"; +} \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 90b61b500..45eeea773 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -8,6 +8,7 @@ public class Md // сохранить позиции их содержимого в Token-ы чтобы при сборке html // поочередно вставлять в StringBuilder html-тэги из токенов и исходный текст. + // В итоге получился довольно большой парсер, но идея работает public static string Render(string input) { var parser = new TokenParser(); diff --git a/cs/Markdown/ParserValidator.cs b/cs/Markdown/ParserValidator.cs index 77d15b1a0..63ff2cdbf 100644 --- a/cs/Markdown/ParserValidator.cs +++ b/cs/Markdown/ParserValidator.cs @@ -2,35 +2,35 @@ namespace Markdown; public class ParserValidator { - private string text; + private string _text; public ParserValidator(string input) { - text = input; + _text = input; } public bool IsMarkCorrect(int startIndex, bool isOpening, int markLength = 1) { - var isScreened = (startIndex > 0 && text[startIndex - 1] == '\\') - && (startIndex > 1 && text[startIndex - 2] != '\\' || startIndex == 1); + var isScreened = (startIndex > 0 && _text[startIndex - 1] == '\\') + && (startIndex > 1 && _text[startIndex - 2] != '\\' || startIndex == 1); if (isOpening) { return !isScreened - && startIndex + markLength < text.Length - && text[startIndex + markLength] != ' '; + && startIndex + markLength < _text.Length + && _text[startIndex + markLength] != ' '; } else { return !isScreened && startIndex > 0 - && text[startIndex - 1] != ' '; + && _text[startIndex - 1] != ' '; } } public bool IsDoubleUnderscore(int index) { - return index < text.Length - 1 && text[index + 1] == '_' - || index > 0 && text[index - 1] == '_'; + return index < _text.Length - 1 && _text[index + 1] == '_' + || index > 0 && _text[index - 1] == '_'; } public bool IsContentAcceptable(string content) @@ -40,9 +40,9 @@ public bool IsContentAcceptable(string content) 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).Contains(' '); + return start > 0 && _text[start - 1] != ' ' + && end < _text.Length - 1 && _text[end + 1] != ' ' + && _text.Substring(start, end - start).Contains(' '); } public bool HasNoDigits(string content) diff --git a/cs/Markdown/TagFactory.cs b/cs/Markdown/TagFactory.cs index 4e793a474..660ade38d 100644 --- a/cs/Markdown/TagFactory.cs +++ b/cs/Markdown/TagFactory.cs @@ -4,22 +4,22 @@ public static class HtmlTagFactory { public static Tag Bold => new("strong"); public static Tag Italic => new("em"); - public static Tag Title => new("h1"); - public static Tag MarkedList => new("li"); + public static Tag Header => new("h1"); + public static Tag List => new("li"); public static Tag BuildTag(string mark) { switch (mark) { - case "#": - return Title; - case "__": + case Marks.Header: + return Header; + case Marks.Bold: return Bold; - case "_": + case Marks.Italic: return Italic; - case "*": - return MarkedList; + case Marks.List: + return List; default: throw new Exception("Wrong mark!"); } diff --git a/cs/Markdown/Tests/Markdown_Tests.cs b/cs/Markdown/Tests/Markdown_Tests.cs index 8db1b243b..1f48da447 100644 --- a/cs/Markdown/Tests/Markdown_Tests.cs +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -13,8 +13,8 @@ public static IEnumerable GenerateHtmlSource() "

    main title

    \nsome bold text", new [] { - new Token("#", "main title", 0, 12), - new Token("__", "some bold text", 13, 31) + new Token(Marks.Header, "main title", 0, 12), + new Token(Marks.Bold, "some bold text", 13, 31) } ).SetName("Simple text"); yield return new TestCaseData( @@ -22,8 +22,8 @@ public static IEnumerable GenerateHtmlSource() "

    main title

    \nsome bold text", new [] { - new Token("#", "main title", 0, 12), - new Token("__", "some bold text", 13, 33) + new Token(Marks.Header, "main title", 0, 12), + new Token(Marks.Bold, "some bold text", 13, 33) } ).SetName("Token inside token"); } @@ -31,7 +31,32 @@ public static IEnumerable GenerateHtmlSource() [Test, TestCaseSource(nameof(GenerateHtmlSource))] public void GenerateHtml_DifferentText(string actual, string expected, Token[] tokens) { - var t = new TokenParser().ParseTokens(actual); Md.GenerateHtml(actual, tokens).Should().Be(expected); } + + public static IEnumerable Render_Source() + { + 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); + } } \ No newline at end of file diff --git a/cs/Markdown/Tests/Tags_Tests.cs b/cs/Markdown/Tests/Tags_Tests.cs index 2105238e4..3104cb0ad 100644 --- a/cs/Markdown/Tests/Tags_Tests.cs +++ b/cs/Markdown/Tests/Tags_Tests.cs @@ -18,7 +18,7 @@ public void Build_Recognize_OnWrongMark() TestCase("#", "h1"), TestCase("_", "em"), TestCase("__", "strong"), - TestCase("*", "li") + TestCase("-", "li") ] public void Build_CorrectTag_OnCorrectMark(string mark, string correctName) { diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs index 2f120712a..2a21250d0 100644 --- a/cs/Markdown/Tests/TokenParser_Tests.cs +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -11,10 +11,10 @@ private void ParseText(string actualInput, Token[] expectedTokens) var parser = new TokenParser(); var actualTokens = parser.ParseTokens(actualInput); var act = () => Md.GenerateHtml(actualInput, actualTokens); - - act.Should().NotThrow(); var actualHtml = Md.GenerateHtml(actualInput, actualTokens); var expectedHtml = Md.GenerateHtml(actualInput, expectedTokens); + + act.Should().NotThrow(); expectedHtml.Should().Be(actualHtml); actualTokens.Should().BeEquivalentTo(expectedTokens); } @@ -26,23 +26,41 @@ public static IEnumerable ParseSimpleText_Source() "__main title__\n__some bold text__", new [] { - new Token("__", "main title", 0, 14), - new Token("__", "some bold text", 15, 33) + new Token(Marks.Bold, "main title", 0, 14), + new Token(Marks.Bold, "some bold text", 15, 33) }).SetName("Bold text"); yield return new TestCaseData( "_main title_\n_some italic text_", new [] { - new Token("_", "main title", 0, 12), - new Token("_", "some italic text", 13, 31) + new Token(Marks.Italic, "main title", 0, 12), + new Token(Marks.Italic, "some italic text", 13, 31) }).SetName("Italic text"); yield return new TestCaseData( "# main title\n# some header text", new [] { - new Token("#", "main title", 0, 12), - new Token("#", "some header text", 13, 31) + new Token(Marks.Header, "main title", 0, 12), + new Token(Marks.Header, "some header text", 13, 31) }).SetName("Headers text"); + yield return new TestCaseData( + "- __bold__\n- _italic_\n- # header", + new [] + { + new Token(Marks.List, "bold", 0, 10), + new Token(Marks.Bold, "bold", 2, 10), + new Token(Marks.List, "italic", 11, 21), + new Token(Marks.Italic, "italic", 13, 21), + new Token(Marks.List, "

    header

    ", 22, 32) + } + ).SetName("List with tags inside"); + yield return new TestCaseData( + "# - 123\n", + new [] + { + new Token(Marks.Header, "
  • 123
  • ", 0, 8), + } + ).SetName("List inside header"); } [Test, TestCaseSource(nameof(ParseSimpleText_Source))] @@ -57,19 +75,19 @@ public static IEnumerable ParseNestingText_Source() "# __main title__\n__some bold text__", new [] { - new Token("#", "main title", 0, 16), - new Token("__", "main title", 2, 16), - new Token("__", "some bold text", 17, 35) + new Token(Marks.Header, "main title", 0, 16), + new Token(Marks.Bold, "main title", 2, 16), + new Token(Marks.Bold, "some bold text", 17, 35) }).SetName("Inside header"); yield return new TestCaseData( "# __main title__\n# __some _bold_ text__", new [] { - new Token("#", "main title", 0, 16), - new Token("__", "main title", 2, 16), - new Token("#", "some bold text", 17, 39), - new Token("__", "some bold text", 19, 39), - new Token("_", "bold", 26, 32) + new Token(Marks.Header, "main title", 0, 16), + new Token(Marks.Bold, "main title", 2, 16), + new Token(Marks.Header, "some bold text", 17, 39), + new Token(Marks.Bold, "some bold text", 19, 39), + new Token(Marks.Italic, "bold", 26, 32) }).SetName("Italic inside Bold"); } @@ -86,17 +104,21 @@ public static IEnumerable ParseTextWithExceptions_Source() "внутри _одинарного __двойное__ не_ работает", new [] { - new Token("_", "одинарного __двойное__ не", 7, 34), - new Token("__", "двойное", 19, 30) + new Token(Marks.Italic, "одинарного __двойное__ не", 7, 34), + new Token(Marks.Bold, "двойное", 19, 30) }).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[] { } + ).SetName("Spaces after/before bold mark"); yield return new TestCaseData( "эти_ подчерки_ не считаются и эти _подчерки _не считаются", new Token[] { } - ).SetName("Spaces after/before mark"); + ).SetName("Spaces after/before italic mark"); yield return new TestCaseData( "выделение в ра_зных сл_овах н__е работ__ает", new Token[] { } @@ -121,7 +143,7 @@ public static IEnumerable ParseTextWithExceptions_Source() @"экран\_ирование\_ и \\__двойное\\__ экрани\рование", new Token[] { - new Token("__", @"двойное\\", 22, 35) + new Token(Marks.Bold, @"двойное\\", 22, 35) } ).SetName("Shielding marks"); } diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs index f339da3a3..b45af08c9 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -1,189 +1,169 @@ +using System.Collections; + namespace Markdown { public class TokenParser { - private List tokens; - private List allFindedTokens; - private string text; - private ParserValidator validator; + private List _tokens; + private HashSet _allFindedTokens; + private string _text; + private ParserValidator _validator; public TokenParser(string text = "") { - this.text = text; - validator = new ParserValidator(text); - tokens = new List(); - allFindedTokens = new List(); + this._text = text; + _validator = new ParserValidator(text); + _tokens = new List(); + _allFindedTokens = new HashSet(); } - public IEnumerable ParseTokens(string input) + public IEnumerable ParseTokens(string input, string outerTokenMark = "") { - this.text = input; - validator = new ParserValidator(input); - tokens = new List(); - allFindedTokens = new List(); + this._text = input; + _validator = new ParserValidator(input); + _tokens = new List(); + _allFindedTokens = new HashSet(); - tokens.AddRange(FindBoldTokens()); - tokens.AddRange(FindItalicTokens()); - tokens.AddRange(FindHeaderTokens()); + AddHeaderTokens(); + AddListTokens(); + AddBoldTokens(outerTokenMark); + AddItalicTokens(outerTokenMark); - tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); - return tokens; + _tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); + return _tokens; + } + + private void AddHeaderTokens(string outerTokenMark = "") + { + _tokens.AddRange(FindParagraphTokens(Marks.Header)); } - private IEnumerable FindBoldTokens() + private void AddListTokens(string outerTokenMark = "") { - var stack = new Stack(); // Храним позиции открывающих тегов + _tokens.AddRange(FindParagraphTokens(Marks.List)); + } - for (var i = 0; i < text.Length - 1; i++) + private void AddBoldTokens(string outerTokenMark) + { + if (outerTokenMark != Marks.Italic && outerTokenMark != Marks.Bold) + { + _tokens.AddRange(FindTokens( + (i, isOpeningTag) => _text[i] == '_' && _text[i+1] == '_' && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Bold.Length), + Marks.Bold + )); + } + } + + private void AddItalicTokens(string outerTokenMark) + { + if (outerTokenMark != Marks.Italic) + { + _tokens.AddRange(FindTokens( + (i, isOpeningTag) => _text[i] == '_' && !_validator.IsDoubleUnderscore(i) && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Italic.Length), + Marks.Italic + )); + } + } + + private IEnumerable FindTokens(Func checkMark, string mark) + { + var stack = new Stack(); + + for (int i = 0; i < _text.Length+1 - mark.Length; i++) { var isOpeningTag = stack.Count == 0; - // Проверяем два символа подряд - if (text[i] == '_' && validator.IsDoubleUnderscore(i) && validator.IsMarkCorrect(i, isOpeningTag, 2)) + + if (checkMark(i, isOpeningTag)) { if (stack.Count > 0) { - // Нашли закрывающий тег var start = stack.Pop(); - var end = i + 2; // +2 потому что два символа - - // Извлекаем содержимое ввиде текста и сразу делаем из него html - var content = text.Substring(start + 2, i - start - 2); - // Проверка на необходимость выделения - if (!validator.IsContentAcceptable(content) || validator.IsSplittingWords(start, end)) + var end = i + mark.Length; + + var content = _text.Substring(start + mark.Length, i - start - mark.Length); + + if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) { continue; } - - // Вписывание вложенных тэгов - var htmlContent = Md.GenerateHtml(content, new TokenParser(content).FindItalicTokens()); - - yield return new Token( - HtmlTagFactory.Bold, + var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); + var token = new Token( + HtmlTagFactory.BuildTag(mark), htmlContent, start, end - ); - i++; // Пропускаем второй символ + ); + + _allFindedTokens.Add(token); + if (SolveOverllaping(start, end)) + yield return token; } else { - // Нашли открывающий тег stack.Push(i); - i++; // Пропускаем второй символ } + i++; } } } - private IEnumerable FindItalicTokens() - { - // Использовал тэг для возможных закрывающих марок - var stack = new Stack(); // Храним позиции открывающих тегов - - for (int i = 0; i < text.Length; i++) - { - var isOpeningTag = stack.Count == 0; - - if (text[i] == '_' && validator.IsMarkCorrect(i, isOpeningTag)) - { - // Проверяем, что это не часть двойного подчеркивания - if (!validator.IsDoubleUnderscore(i)) - { - if (stack.Count > 0) - { - // Нашли закрывающий тег - int start = stack.Pop(); - int end = i + 1; - - // Извлекаем содержимое (без "марок") - string content = text.Substring(start + 1, i - start - 1); - - // Проверка на необходимость выделения - if (!validator.IsContentAcceptable(content) || validator.IsSplittingWords(start, end)) - { - continue; - } - - var token = new Token( - HtmlTagFactory.Italic, - content, - start, - end - ); - // Проверяем, что этот тег не пересекается с уже найденными - if (!CheckOverlappingWithExistingTokens(start, end)) - { - yield return token; - } - else - { - allFindedTokens.Add(token); - } - } - else - { - // Нашли открывающий тег - stack.Push(i); - } - } - } - } - } - - private IEnumerable FindHeaderTokens() + private IEnumerable FindParagraphTokens(string mark) { // Обрабатываем текст построчно для заголовков int lineStart = 0; - for (int i = 0; i < text.Length; i++) + for (int i = 0; i < _text.Length; i++) { - if (text[i] == '\n' || i == text.Length - 1) + if (_text[i] == '\n' || i == _text.Length - 1) { // Определяем конец строки - int lineEnd = (i == text.Length - 1) ? i + 1 : i; - int lineLength = lineEnd - lineStart; + int lineEnd = (i == _text.Length - 1) ? i + 1 : i; + foreach (var token in ParseLineToTokens(lineStart, lineEnd, mark)) + yield return token; - if (lineLength > 0) - { - foreach (var token in ProcessHeaderLine(text, lineStart, lineEnd)) - { - yield return token; - } - } - - lineStart = i + 1; // Начало следующей строки + lineStart = i + 1; } } // Обрабатываем последнюю строку, если текст не заканчивается \n - if (lineStart < text.Length) + foreach (var token in ParseLineToTokens(lineStart, _text.Length, mark)) + yield return token; + } + + private IEnumerable ParseLineToTokens(int lineStart, int lineEnd, string mark) + { + int lineLength = lineEnd - lineStart; + + if (lineLength > 0) { - foreach (var token in ProcessHeaderLine(text, lineStart, text.Length)) + foreach (var token in FindTokensInLine(lineStart, lineEnd, mark)) { - yield return token; + _allFindedTokens.Add(token); + if (SolveOverllaping(token.StartPosition, token.EndPosition)) + yield return token; } } } - private IEnumerable ProcessHeaderLine(string input, int lineStart, int lineEnd) + + private IEnumerable FindTokensInLine(int lineStart, int lineEnd, string mark) { // Если есть # и пробел int pos = lineStart; - while (pos < lineEnd && input[pos] != '#' && !char.IsWhiteSpace(input[pos+1])) + while (pos < lineEnd - 1 && _text[pos].ToString() != mark && char.IsWhiteSpace(_text[pos+1])) { pos++; } - if (pos < lineEnd && input[pos] == '#') + if (pos < lineEnd && _text[pos].ToString() == mark) { // Пропускаем пробел после # pos++; - // Извлекаем содержимое заголовка - string content = input.Substring(pos, lineEnd - pos).Trim(); - var htmlContent = Md.GenerateHtml(content, new TokenParser(text).ParseTokens(content)); + string content = _text.Substring(pos, lineEnd - pos).Trim(); + var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); yield return new Token( - HtmlTagFactory.Title, + HtmlTagFactory.BuildTag(mark), htmlContent, pos - 1, lineEnd @@ -191,12 +171,24 @@ private IEnumerable ProcessHeaderLine(string input, int lineStart, int li } } - private bool CheckOverlappingWithExistingTokens(int start, int end) + private bool SolveOverllaping(int start, int end) + { + var overlapping = GetOverlappingTokens(start, end); + if (!overlapping.Any()) + { + return true; + } + else + { + ClearOverlappingTokens(overlapping); + return false; + } + } + + private IEnumerable GetOverlappingTokens(int start, int end) { - var isOverlapping = false; - var toDelete = new List(); - allFindedTokens.AddRange(tokens); - foreach (var token in allFindedTokens) + var overlappingTokens = new List(); + foreach (var token in _allFindedTokens) { if ( end > token.EndPosition @@ -205,20 +197,17 @@ private bool CheckOverlappingWithExistingTokens(int start, int end) && start < token.StartPosition ) { - // Удаляем токены пересекаемые найденным - toDelete.Add(token); - isOverlapping = true; + overlappingTokens.Add(token); } } - ClearOverlappingToken(toDelete); - return isOverlapping; + return overlappingTokens; } - private void ClearOverlappingToken(IEnumerable toDelete) + private void ClearOverlappingTokens(IEnumerable toDelete) { foreach (var tokenToRemove in toDelete) { - tokens.Remove(tokenToRemove); + _tokens.Remove(tokenToRemove); } } } From 666bc8fe3efde53ac8e73bd59a5f25f52bde9592 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Tue, 11 Nov 2025 12:41:07 +0500 Subject: [PATCH 06/12] new tests --- cs/Markdown/Tests/TokenParser_Tests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs index 2a21250d0..cebdb8bd7 100644 --- a/cs/Markdown/Tests/TokenParser_Tests.cs +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -111,6 +111,15 @@ public static IEnumerable ParseTextWithExceptions_Source() "c цифрами_12_3 не считаются выделением __даже1так__", new Token[] { } ).SetName("Numbers aren't tagged"); + yield return new TestCaseData( + "_нач_ало се_ред_ина ко_нец_", + new Token[] + { + new Token(Marks.Italic, "нач", 0, 5), + new Token(Marks.Italic, "ред", 11, 16), + new Token(Marks.Italic, "нец", 22, 27), + } + ).SetName("Parts of words"); yield return new TestCaseData( "эти__ подчерки__ не считаются и эти __подчерки __не считаются", new Token[] { } From ee02f3ccb8f211bebc05467279607023627ca5fc Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Tue, 11 Nov 2025 23:53:21 +0500 Subject: [PATCH 07/12] stack half way --- cs/Markdown/Marks.cs | 25 ++ cs/Markdown/Md.cs | 57 ++- cs/Markdown/PositionedTag.cs | 15 + cs/Markdown/Tag.cs | 19 +- cs/Markdown/TagFactory.cs | 49 +-- cs/Markdown/Tests/Markdown_Tests.cs | 30 +- cs/Markdown/Tests/Tags_Tests.cs | 8 +- cs/Markdown/Tests/TokenParser_Tests.cs | 2 +- cs/Markdown/Token.cs | 63 ++-- cs/Markdown/TokenParser.cs | 480 ++++++++++++++++--------- 10 files changed, 487 insertions(+), 261 deletions(-) create mode 100644 cs/Markdown/PositionedTag.cs diff --git a/cs/Markdown/Marks.cs b/cs/Markdown/Marks.cs index 21971411c..838eff99c 100644 --- a/cs/Markdown/Marks.cs +++ b/cs/Markdown/Marks.cs @@ -6,4 +6,29 @@ public static class Marks public const string Italic = "_"; public const string Header = "#"; public const string List = "-"; + + public static 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!"); + } + } } \ No newline at end of file diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 45eeea773..fe9ae1a1d 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -1,42 +1,41 @@ using System.Text; -namespace Markdown +namespace Markdown; + +public class Md { - public class Md - { - // Моя идея заключается в том, что найдя все действующие "inline elements" - // сохранить позиции их содержимого в Token-ы чтобы при сборке html - // поочередно вставлять в StringBuilder html-тэги из токенов и исходный текст. + // Моя идея заключается в том, что найдя все действующие "inline elements" + // сохранить позиции их содержимого в Token-ы чтобы при сборке html + // поочередно вставлять в StringBuilder html-тэги из токенов и исходный текст. - // В итоге получился довольно большой парсер, но идея работает - public static string Render(string input) - { - var parser = new TokenParser(); - var tokens = parser.ParseTokens(input); + // В итоге получился довольно большой парсер, но идея работает + public static string Render(string input) + { + var parser = new TokenParser(); + var tokens = parser.ParseTokens(input); - return GenerateHtml(input, tokens); - } + return GenerateHtml(input, tokens); + } - public static string GenerateHtml(string text, IEnumerable tokens) + public static string GenerateHtml(string text, IEnumerable tokens) + { + var mergedText = new StringBuilder(); + var prevPosition = 0; + var nextPositionMin = -1; + foreach (var token in tokens) { - var mergedText = new StringBuilder(); - var prevPosition = 0; - var nextPositionMin = -1; - foreach (var token in tokens) + var currentPosition = token.StartPosition; + if (currentPosition < nextPositionMin) { - var currentPosition = token.StartPosition; - if (currentPosition < nextPositionMin) - { - continue; - } - mergedText.Append(text.Substring(prevPosition,currentPosition-prevPosition)); - mergedText.Append(token.ToString()); - prevPosition = token.EndPosition; - nextPositionMin = token.EndPosition; + continue; } - mergedText.Append(text.Substring(prevPosition)); - return mergedText.ToString(); + mergedText.Append(text.Substring(prevPosition,currentPosition-prevPosition)); + mergedText.Append(token.ToString()); + prevPosition = token.EndPosition; + nextPositionMin = token.EndPosition; } + mergedText.Append(text.Substring(prevPosition)); + return mergedText.ToString(); } } diff --git a/cs/Markdown/PositionedTag.cs b/cs/Markdown/PositionedTag.cs new file mode 100644 index 000000000..986017bb7 --- /dev/null +++ b/cs/Markdown/PositionedTag.cs @@ -0,0 +1,15 @@ +namespace Markdown; + +public class PositionedTag : Tag +{ + public int Position { get; } + public bool IsOpenClose { get; } + public bool IsOpening { get; } + + 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/Tag.cs b/cs/Markdown/Tag.cs index c4bbc5935..8083aede0 100644 --- a/cs/Markdown/Tag.cs +++ b/cs/Markdown/Tag.cs @@ -1,16 +1,13 @@ -namespace Markdown +namespace Markdown; + +public class Tag { + public string Name { get; } + public string OpenTag => $"<{Name}>"; + public string CloseTag => $""; - public class Tag + public Tag(string name) { - private string name; - public string Name { get { return name; } } - public string OpenTag => $"<{Name}>"; - public string CloseTag => $""; - - public Tag(string name) - { - this.name = name; - } + this.Name = name; } } \ No newline at end of file diff --git a/cs/Markdown/TagFactory.cs b/cs/Markdown/TagFactory.cs index 660ade38d..52e942b44 100644 --- a/cs/Markdown/TagFactory.cs +++ b/cs/Markdown/TagFactory.cs @@ -1,28 +1,33 @@ -namespace Markdown -{ - public static class HtmlTagFactory - { - public static Tag Bold => new("strong"); - public static Tag Italic => new("em"); - public static Tag Header => new("h1"); - public static Tag List => new("li"); +namespace Markdown; +public static class TagNames { + public const string Strong = "strong"; + public const string Em = "em"; + public const string Header = "header"; + public const string List = "li"; +} - public static Tag BuildTag(string mark) +public static class TagFactory +{ + public static Tag Bold => new(TagNames.Strong); + public static Tag Italic => new(TagNames.Em); + public static Tag Header => new(TagNames.Header); + public static Tag List => new(TagNames.List); + + public static Tag BuildTag(string mark) + { + switch (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 Exception("Wrong 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/Tests/Markdown_Tests.cs b/cs/Markdown/Tests/Markdown_Tests.cs index 1f48da447..d63ad006d 100644 --- a/cs/Markdown/Tests/Markdown_Tests.cs +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -1,7 +1,9 @@ +using System.Diagnostics; using FluentAssertions; using NUnit.Framework; +using System.Text; -namespace Markdown; +namespace Markdown.Tests; [TestFixture] class Markdown_Tests @@ -59,4 +61,30 @@ public void Render_DifferentText(string actual, string expected) { Md.Render(actual).Should().Be(expected); } + + [Test] + public void EffiecencyTest() + { + var stopwatch = Stopwatch.StartNew(); + Md.Render(StackString(10000)); + stopwatch.Stop(); + var time1 = stopwatch.ElapsedMilliseconds * 10 - 1000; + + stopwatch.Restart(); + Md.Render(StackString(100000)); + stopwatch.Stop(); + var time2 = stopwatch.ElapsedMilliseconds; + time2.Should().BeLessThan(time1); + } + + private string StackString(int times) + { + var sb = new StringBuilder(); + for (int i = 0; i < 10000; 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 index 3104cb0ad..ba91756aa 100644 --- a/cs/Markdown/Tests/Tags_Tests.cs +++ b/cs/Markdown/Tests/Tags_Tests.cs @@ -1,7 +1,7 @@ using FluentAssertions; using NUnit.Framework; -namespace Markdown; +namespace Markdown.Tests; [TestFixture] class Tags_Tests @@ -9,7 +9,7 @@ class Tags_Tests [Test] public void Build_Recognize_OnWrongMark() { - var act = () => HtmlTagFactory.BuildTag("1"); + var act = () => TagFactory.BuildTag("1"); act.Should().Throw(); } @@ -22,7 +22,7 @@ public void Build_Recognize_OnWrongMark() ] public void Build_CorrectTag_OnCorrectMark(string mark, string correctName) { - var tag = HtmlTagFactory.BuildTag(mark); + var tag = TagFactory.BuildTag(mark); var correctTag = new Tag(correctName); tag.Should().BeEquivalentTo(correctTag); @@ -31,7 +31,7 @@ public void Build_CorrectTag_OnCorrectMark(string mark, string correctName) [Test] public void OpenCloseOutput_IsCorrect() { - var tag = HtmlTagFactory.BuildTag("#"); + var tag = TagFactory.BuildTag("#"); var text = tag.OpenTag + "text" + tag.CloseTag; text.Should().Be("

    text

    "); diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs index cebdb8bd7..ec99afdc0 100644 --- a/cs/Markdown/Tests/TokenParser_Tests.cs +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -1,7 +1,7 @@ using FluentAssertions; using NUnit.Framework; -namespace Markdown; +namespace Markdown.Tests; [TestFixture] class TokenParser_Tests diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 3779ae23e..a7fd8a2a7 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -1,41 +1,40 @@ using System.Text; using NUnit.Framework; -namespace Markdown +namespace Markdown; + +public class Token { - public class Token + private string value; + private Tag tag; + public string Head => tag.OpenTag; + public string Tail => tag.CloseTag; + public int Length => value.Length; + + public int StartPosition { get; } + + public int EndPosition { get; } + + public int OriginalLength => EndPosition - StartPosition; + + public Token(Tag tag, string value, int start, int end) { - private string value; - private Tag tag; - private int start; - private int end; - public string Head => tag.OpenTag; - public string Tail => tag.CloseTag; - public int Length => value.Length; - - public int StartPosition => start; - public int EndPosition => end; - public int OriginalLength => EndPosition - StartPosition; + this.tag = tag; + this.value = value; + this.StartPosition = start; + this.EndPosition = end; + } - public Token(Tag tag, string value, int start, int end) - { - this.tag = tag; - this.value = value; - this.start = start; - this.end = end; - } + public Token(string mark, string value, int start, int end) : this(TagFactory.BuildTag(mark), value, start, end) + { + } - public Token(string mark, string value, int start, int end) : this(HtmlTagFactory.BuildTag(mark), value, start, end) - { - } - - public override string ToString() - { - var builder = new StringBuilder(); - builder.Append(Head); - builder.Append(value); - builder.Append(Tail); - return builder.ToString(); - } + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(Head); + builder.Append(value); + builder.Append(Tail); + return builder.ToString(); } } \ No newline at end of file diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs index b45af08c9..df20798c0 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -1,214 +1,372 @@ using System.Collections; -namespace Markdown +namespace Markdown; + +public class TokenParser { - public class TokenParser + private List _tokens; + private HashSet _allFindedTokens; + private string _text; + private ParserValidator _validator; + private Stack _tagStack; + + public TokenParser(string text = "") { - private List _tokens; - private HashSet _allFindedTokens; - private string _text; - private ParserValidator _validator; + _text = text; + _validator = new (text); + _tokens = new (); + _allFindedTokens = new (); + _tagStack = new (); + } + + public IEnumerable ParseTokens(string input, string outerTokenMark = "") + { + _text = input; + _validator = new ParserValidator(input); + _tokens = new List(); + _allFindedTokens = new HashSet(); + _tagStack = new Stack(); - public TokenParser(string text = "") - { - this._text = text; - _validator = new ParserValidator(text); - _tokens = new List(); - _allFindedTokens = new HashSet(); - } - - public IEnumerable ParseTokens(string input, string outerTokenMark = "") - { - this._text = input; - _validator = new ParserValidator(input); - _tokens = new List(); - _allFindedTokens = new HashSet(); - - AddHeaderTokens(); - AddListTokens(); - AddBoldTokens(outerTokenMark); - AddItalicTokens(outerTokenMark); - - _tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); - return _tokens; - } - - private void AddHeaderTokens(string outerTokenMark = "") - { - _tokens.AddRange(FindParagraphTokens(Marks.Header)); - } - - private void AddListTokens(string outerTokenMark = "") - { - _tokens.AddRange(FindParagraphTokens(Marks.List)); - } + _tokens.AddRange(FindAllTokens()); - private void AddBoldTokens(string outerTokenMark) - { - if (outerTokenMark != Marks.Italic && outerTokenMark != Marks.Bold) - { - _tokens.AddRange(FindTokens( - (i, isOpeningTag) => _text[i] == '_' && _text[i+1] == '_' && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Bold.Length), - Marks.Bold - )); - } - } - - private void AddItalicTokens(string outerTokenMark) - { - if (outerTokenMark != Marks.Italic) - { - _tokens.AddRange(FindTokens( - (i, isOpeningTag) => _text[i] == '_' && !_validator.IsDoubleUnderscore(i) && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Italic.Length), - Marks.Italic - )); - } - } + _tokens.Sort((x, y) => x.StartPosition.CompareTo(y.StartPosition)); + return _tokens; + } - private IEnumerable FindTokens(Func checkMark, string mark) + private IEnumerable FindAllTokens() + { + for (int i = 0; i < _text.Length; i++) { - var stack = new Stack(); - - for (int i = 0; i < _text.Length+1 - mark.Length; i++) + var findedTag = FindTag(i); + if (findedTag != null) { - var isOpeningTag = stack.Count == 0; - - if (checkMark(i, isOpeningTag)) + // что то уже лежит + if (_tagStack.Count > 0) { - if (stack.Count > 0) + var lastTag = _tagStack.Pop(); + if (lastTag.Name == findedTag.Name) { - var start = stack.Pop(); - var end = i + mark.Length; - - var content = _text.Substring(start + mark.Length, i - start - mark.Length); - - if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) + if (lastTag.IsOpening && (!findedTag.IsOpening || findedTag.IsOpenClose)) + { + var token = BuildToken(lastTag, findedTag); + if (token is not null) + yield return token; + } + else { - continue; + _tagStack.Push(findedTag); } - var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); - var token = new Token( - HtmlTagFactory.BuildTag(mark), - htmlContent, - start, - end - ); - - _allFindedTokens.Add(token); - if (SolveOverllaping(start, end)) - yield return token; } else { - stack.Push(i); + if (!findedTag.IsOpening && !findedTag.IsOpenClose) + { + _tagStack.Push(lastTag); + } + else + { + + _tagStack.Push(findedTag); + } } - i++; } + else + { + // Закрывающие не кладем + if (findedTag.IsOpening) + _tagStack.Push(findedTag); + } + i++; } } + } + + private PositionedTag? FindTag(int index) + { + foreach (var mark in Marks.AllMarks) + { + var tag = GetPositionedTag(index, mark); + if (tag is not null) + return tag; + } + + return null; + } + + #region GetPositionedTag + + private PositionedTag? GetPositionedTag(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); - private IEnumerable FindParagraphTokens(string mark) + return null; + } + + private bool CheckByMark(int index, string mark, bool isOpening) + { + switch (mark) { - // Обрабатываем текст построчно для заголовков - int lineStart = 0; + 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]); + } + + private bool CheckMarkForList(int index, bool isOpening) + { + return isOpening + && index + 1 < _text.Length + && _text[index].ToString() == Marks.Header + && char.IsWhiteSpace(_text[index+1]); + } + + #endregion + + private Token? BuildToken(PositionedTag startTag, PositionedTag endTag) + { + var mark = Marks.GetMarkByTagName(startTag.Name); + var start = startTag.Position; + var end = endTag.Position + mark.Length; + + var content = _text.Substring(start + mark.Length, endTag.Position - start - mark.Length); - for (int i = 0; i < _text.Length; i++) - { - if (_text[i] == '\n' || i == _text.Length - 1) - { - // Определяем конец строки - int lineEnd = (i == _text.Length - 1) ? i + 1 : i; - foreach (var token in ParseLineToTokens(lineStart, lineEnd, mark)) - yield return token; - - lineStart = i + 1; - } - } + if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) + { + return null; + } - // Обрабатываем последнюю строку, если текст не заканчивается \n - foreach (var token in ParseLineToTokens(lineStart, _text.Length, mark)) - yield return token; + var token = new Token( + TagFactory.BuildTag(mark), + content, + start, + end + ); + return token; + // _allFindedTokens.Add(token); + // if (SolveOverllaping(start, end)) + // return token; + // return null; + } + + # region commented + /* + private void AddHeaderTokens(string outerTokenMark = "") + { + _tokens.AddRange(FindParagraphTokens(Marks.Header)); + } + + private void AddListTokens(string outerTokenMark = "") + { + _tokens.AddRange(FindParagraphTokens(Marks.List)); + } + + private void AddBoldTokens(string outerTokenMark) + { + if (outerTokenMark != Marks.Italic && outerTokenMark != Marks.Bold) + { + _tokens.AddRange(FindTokens( + (i, isOpeningTag) => _text[i] == '_' && _text[i+1] == '_' && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Bold.Length), + Marks.Bold + )); + } + } + private void AddItalicTokens(string outerTokenMark) + { + if (outerTokenMark != Marks.Italic) + { + _tokens.AddRange(FindTokens( + (i, isOpeningTag) => _text[i] == '_' && !_validator.IsDoubleUnderscore(i) && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Italic.Length), + Marks.Italic + )); } + } + + + private IEnumerable FindTokens(Func checkMark, string mark) + { + var stack = new Stack(); - private IEnumerable ParseLineToTokens(int lineStart, int lineEnd, string mark) + for (int i = 0; i < _text.Length+1 - mark.Length; i++) { - int lineLength = lineEnd - lineStart; - - if (lineLength > 0) + var isOpeningTag = stack.Count == 0; + + if (checkMark(i, isOpeningTag)) { - foreach (var token in FindTokensInLine(lineStart, lineEnd, mark)) + if (stack.Count > 0) { + var start = stack.Pop(); + var end = i + mark.Length; + + var content = _text.Substring(start + mark.Length, i - start - mark.Length); + + if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) + { + continue; + } + var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); + var token = new Token( + TagFactory.BuildTag(mark), + htmlContent, + start, + end + ); + _allFindedTokens.Add(token); - if (SolveOverllaping(token.StartPosition, token.EndPosition)) + if (SolveOverllaping(start, end)) yield return token; } + else + { + stack.Push(i); + } + i++; } } - - private IEnumerable FindTokensInLine(int lineStart, int lineEnd, string mark) + } + + private IEnumerable FindParagraphTokens(string mark) + { + // Обрабатываем текст построчно для заголовков + int lineStart = 0; + + for (int i = 0; i < _text.Length; i++) { - // Если есть # и пробел - int pos = lineStart; - while (pos < lineEnd - 1 && _text[pos].ToString() != mark && char.IsWhiteSpace(_text[pos+1])) - { - pos++; - } - - if (pos < lineEnd && _text[pos].ToString() == mark) + if (_text[i] == '\n' || i == _text.Length - 1) { - // Пропускаем пробел после # - pos++; + // Определяем конец строки + int lineEnd = (i == _text.Length - 1) ? i + 1 : i; + foreach (var token in ParseLineToTokens(lineStart, lineEnd, mark)) + yield return token; - string content = _text.Substring(pos, lineEnd - pos).Trim(); - var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); - - yield return new Token( - HtmlTagFactory.BuildTag(mark), - htmlContent, - pos - 1, - lineEnd - ); + lineStart = i + 1; } } - - private bool SolveOverllaping(int start, int end) + + // Обрабатываем последнюю строку, если текст не заканчивается \n + foreach (var token in ParseLineToTokens(lineStart, _text.Length, mark)) + yield return token; + } + + private IEnumerable ParseLineToTokens(int lineStart, int lineEnd, string mark) + { + int lineLength = lineEnd - lineStart; + + if (lineLength > 0) { - var overlapping = GetOverlappingTokens(start, end); - if (!overlapping.Any()) - { - return true; - } - else + foreach (var token in FindTokensInLine(lineStart, lineEnd, mark)) { - ClearOverlappingTokens(overlapping); - return false; + _allFindedTokens.Add(token); + if (SolveOverllaping(token.StartPosition, token.EndPosition)) + yield return token; } } + } + + private IEnumerable FindTokensInLine(int lineStart, int lineEnd, string mark) + { + // Если есть # и пробел + int pos = lineStart; + while (pos < lineEnd - 1 && _text[pos].ToString() != mark && char.IsWhiteSpace(_text[pos+1])) + { + pos++; + } + + if (pos < lineEnd && _text[pos].ToString() == mark) + { + // Пропускаем пробел после # + pos++; - private IEnumerable GetOverlappingTokens(int start, int end) + string content = _text.Substring(pos, lineEnd - pos).Trim(); + var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); + + yield return new Token( + TagFactory.BuildTag(mark), + htmlContent, + pos - 1, + lineEnd + ); + } + } + */ + #endregion + + private bool SolveOverllaping(int start, int end) + { + var overlapping = GetOverlappingTokens(start, end); + if (!overlapping.Any()) + { + return true; + } + else + { + ClearOverlappingTokens(overlapping); + return false; + } + } + + private IEnumerable GetOverlappingTokens(int start, int end) + { + var overlappingTokens = new List(); + foreach (var token in _allFindedTokens) { - var overlappingTokens = new List(); - foreach (var token in _allFindedTokens) + if ( + end > token.EndPosition + && token.StartPosition < start && start < token.EndPosition + || token.StartPosition < end && end < token.EndPosition + && start < token.StartPosition + ) { - if ( - end > token.EndPosition - && token.StartPosition < start && start < token.EndPosition - || token.StartPosition < end && end < token.EndPosition - && start < token.StartPosition - ) - { - overlappingTokens.Add(token); - } + overlappingTokens.Add(token); } - return overlappingTokens; } + return overlappingTokens; + } - private void ClearOverlappingTokens(IEnumerable toDelete) + private void ClearOverlappingTokens(IEnumerable toDelete) + { + foreach (var tokenToRemove in toDelete) { - foreach (var tokenToRemove in toDelete) - { - _tokens.Remove(tokenToRemove); - } + _tokens.Remove(tokenToRemove); } } } \ No newline at end of file From 2df5c3a63b6750e5ee46d5eb85d483028d3222c2 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Wed, 12 Nov 2025 02:06:51 +0500 Subject: [PATCH 08/12] first passed --- cs/Markdown/Marks.cs | 7 ++ cs/Markdown/PositionedTag.cs | 1 + cs/Markdown/TokenParser.cs | 177 +++++++++++++++++++---------------- 3 files changed, 105 insertions(+), 80 deletions(-) diff --git a/cs/Markdown/Marks.cs b/cs/Markdown/Marks.cs index 838eff99c..fc892d015 100644 --- a/cs/Markdown/Marks.cs +++ b/cs/Markdown/Marks.cs @@ -31,4 +31,11 @@ public static string GetMarkByTagName(string name) 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/PositionedTag.cs b/cs/Markdown/PositionedTag.cs index 986017bb7..c1cad32dd 100644 --- a/cs/Markdown/PositionedTag.cs +++ b/cs/Markdown/PositionedTag.cs @@ -5,6 +5,7 @@ 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) { diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs index df20798c0..d1c5516ab 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -37,55 +37,92 @@ 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) + if (findedTag == null) + continue; + + // что-то уже лежит + if (_tagStack.Count > 0) { - // что то уже лежит - if (_tagStack.Count > 0) + var lastTag = _tagStack.Pop(); + if (lastTag.Name == findedTag.Name) { - var lastTag = _tagStack.Pop(); - if (lastTag.Name == findedTag.Name) + // types equal check if open && close + if (lastTag.IsOpening && (!findedTag.IsOpening || findedTag.IsOpenClose)) { - if (lastTag.IsOpening && (!findedTag.IsOpening || findedTag.IsOpenClose)) - { - var token = BuildToken(lastTag, findedTag); - if (token is not null) - yield return token; - } - else - { - _tagStack.Push(findedTag); - } + var token = BuildTokenOrNull(lastTag, findedTag); + if (token is not null) + yield return token; } else + // two open or close in a row -> use second opener and first closer + PushIfOpened(findedTag); + } + else + { + // tags different -> closing = cross + if (!findedTag.IsOpening && !findedTag.IsOpenClose) { - if (!findedTag.IsOpening && !findedTag.IsOpenClose) - { - _tagStack.Push(lastTag); - } - else + _tagStack.Push(lastTag); + _tagStack.Push(findedTag); + } + else + { + // check if possible incapsulate + var currentMark = findedTag.Mark; + var lastMark = lastTag.Mark; + if (lastMark != Marks.Italic && lastMark != currentMark) { - _tagStack.Push(findedTag); } } } - else - { - // Закрывающие не кладем - if (findedTag.IsOpening) - _tagStack.Push(findedTag); - } - i++; } + else + PushIfOpened(findedTag); + i++; + } + foreach (var token in BuildTokensFromStack(_text.Length)) + { + yield return token; } } + private IEnumerable BuildTokensFromStack(int lineEnd) + { + while (_tagStack.Count > 0) + { + var current = _tagStack.Pop(); + if (current.Mark == Marks.Header || current.Mark == Marks.List) + { + var token = BuildTokenOrNull(current, new PositionedTag(lineEnd+1, current.Mark, false)); + if (token is not null) + yield return token; + } + + } + } + + private void PushIfOpened(PositionedTag findedTag) + { + if (findedTag.IsOpening) + _tagStack.Push(findedTag); + } + private PositionedTag? FindTag(int index) { foreach (var mark in Marks.AllMarks) { - var tag = GetPositionedTag(index, mark); + var tag = GetPositionedTagOrNull(index, mark); if (tag is not null) return tag; } @@ -95,7 +132,7 @@ private IEnumerable FindAllTokens() #region GetPositionedTag - private PositionedTag? GetPositionedTag(int index, string mark) + private PositionedTag? GetPositionedTagOrNull(int index, string mark) { var isOpening = CheckByMark(index, mark, true); var isClosing = CheckByMark(index, mark, false); @@ -106,7 +143,7 @@ private IEnumerable FindAllTokens() if (isOpening) return new PositionedTag(index, mark, isOpening:isOpening); if (isClosing) - return new PositionedTag(index, mark, isOpening:!isOpening); + return new PositionedTag(index, mark, isOpening:isOpening); return null; } @@ -162,13 +199,13 @@ private bool CheckMarkForList(int index, bool isOpening) #endregion - private Token? BuildToken(PositionedTag startTag, PositionedTag endTag) + private Token? BuildTokenOrNull(PositionedTag startTag, PositionedTag endTag) { - var mark = Marks.GetMarkByTagName(startTag.Name); + var mark = startTag.Mark; var start = startTag.Position; - var end = endTag.Position + mark.Length; + var end = endTag.Position + mark.Length - 2 * Marks.AfterMarkSpace(mark); - var content = _text.Substring(start + mark.Length, endTag.Position - start - mark.Length); + var content = _text.Substring(start + mark.Length + Marks.AfterMarkSpace(mark), endTag.Position - start - mark.Length - 2 * Marks.AfterMarkSpace(mark)); if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) { @@ -181,11 +218,31 @@ private bool CheckMarkForList(int index, bool isOpening) start, end ); - return token; - // _allFindedTokens.Add(token); - // if (SolveOverllaping(start, end)) - // return token; - // return null; + + // возможно рудимент + _allFindedTokens.Add(token); + if (!SolveOverllaping(start, end)) + return token; + return null; + } + + private bool SolveOverllaping(int start, int end) + { + var isOverlapping = false; + foreach (var token in _allFindedTokens) + { + if ( + end > token.EndPosition + && token.StartPosition < start && start < token.EndPosition + || token.StartPosition < end && end < token.EndPosition + && start < token.StartPosition + ) + { + _tokens.Remove(token); + isOverlapping = true; + } + } + return isOverlapping; } # region commented @@ -329,44 +386,4 @@ private IEnumerable FindTokensInLine(int lineStart, int lineEnd, string m } */ #endregion - - private bool SolveOverllaping(int start, int end) - { - var overlapping = GetOverlappingTokens(start, end); - if (!overlapping.Any()) - { - return true; - } - else - { - ClearOverlappingTokens(overlapping); - return false; - } - } - - private IEnumerable GetOverlappingTokens(int start, int end) - { - var overlappingTokens = new List(); - foreach (var token in _allFindedTokens) - { - if ( - end > token.EndPosition - && token.StartPosition < start && start < token.EndPosition - || token.StartPosition < end && end < token.EndPosition - && start < token.StartPosition - ) - { - overlappingTokens.Add(token); - } - } - return overlappingTokens; - } - - private void ClearOverlappingTokens(IEnumerable toDelete) - { - foreach (var tokenToRemove in toDelete) - { - _tokens.Remove(tokenToRemove); - } - } } \ No newline at end of file From 5f0fc0a5aafe70fe7e274e8766ccee3b0d691dc3 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Wed, 12 Nov 2025 17:06:07 +0500 Subject: [PATCH 09/12] all ruined --- cs/Markdown/Md.cs | 42 +++++++++++++++++++++++--- cs/Markdown/ParserValidator.cs | 7 +++-- cs/Markdown/TagFactory.cs | 2 +- cs/Markdown/Tests/Markdown_Tests.cs | 4 +++ cs/Markdown/Tests/TokenParser_Tests.cs | 39 +++++++++++++++--------- cs/Markdown/Token.cs | 20 ++++++------ cs/Markdown/TokenParser.cs | 24 ++++----------- 7 files changed, 86 insertions(+), 52 deletions(-) diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index fe9ae1a1d..30ac8d5c8 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -4,11 +4,6 @@ namespace Markdown; public class Md { - // Моя идея заключается в том, что найдя все действующие "inline elements" - // сохранить позиции их содержимого в Token-ы чтобы при сборке html - // поочередно вставлять в StringBuilder html-тэги из токенов и исходный текст. - - // В итоге получился довольно большой парсер, но идея работает public static string Render(string input) { var parser = new TokenParser(); @@ -18,6 +13,43 @@ public static string Render(string input) } 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.EndPosition > outerEnd) + { + mergedText.Append(text.Substring(prevPosition,token.EndPosition - prevPosition - 2)); + mergedText.Append(token.Tail); + prevPosition = token.EndPosition; + } + else + { + mergedText.Append(text.Substring(prevPosition,token.StartPosition-prevPosition)); + mergedText.Append(token.Head); + prevPosition = token.StartPosition + 2; + outerEnd = token.EndPosition; + tokenStack.Push(token); + } + } + + while (tokenStack.Count > 0) + { + var token = tokenStack.Pop(); + mergedText.Append(text.Substring(prevPosition - token.MarkLength,token.EndPosition - prevPosition)); + mergedText.Append(token.Tail); + prevPosition = token.EndPosition + 2; + } + prevPosition -= 3; + mergedText.Append(text.Substring(prevPosition, text.Length - prevPosition)); + return mergedText.ToString(); + } + + public static string GenerateHtmlOld(string text, IEnumerable tokens) { var mergedText = new StringBuilder(); var prevPosition = 0; diff --git a/cs/Markdown/ParserValidator.cs b/cs/Markdown/ParserValidator.cs index 63ff2cdbf..64b34cf80 100644 --- a/cs/Markdown/ParserValidator.cs +++ b/cs/Markdown/ParserValidator.cs @@ -33,16 +33,17 @@ public bool IsDoubleUnderscore(int index) || index > 0 && _text[index - 1] == '_'; } - public bool IsContentAcceptable(string content) + public bool IsContentAcceptable(string content, string mark) { - return !string.IsNullOrEmpty(content) && HasNoDigits(content); + 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).Contains(' '); + && _text.Substring(start, end - start).Contains(' ') + && _text[end] != '\n'; } public bool HasNoDigits(string content) diff --git a/cs/Markdown/TagFactory.cs b/cs/Markdown/TagFactory.cs index 52e942b44..7f3c9f3b5 100644 --- a/cs/Markdown/TagFactory.cs +++ b/cs/Markdown/TagFactory.cs @@ -3,7 +3,7 @@ namespace Markdown; public static class TagNames { public const string Strong = "strong"; public const string Em = "em"; - public const string Header = "header"; + public const string Header = "h1"; public const string List = "li"; } diff --git a/cs/Markdown/Tests/Markdown_Tests.cs b/cs/Markdown/Tests/Markdown_Tests.cs index d63ad006d..100920998 100644 --- a/cs/Markdown/Tests/Markdown_Tests.cs +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -38,6 +38,10 @@ public void GenerateHtml_DifferentText(string actual, string expected, Token[] t public static IEnumerable Render_Source() { + yield return new TestCaseData( + "п# з __ж _к_ ж__ з\nп", + "п

    з ж к ж з

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

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

    " diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs index ec99afdc0..8fc780119 100644 --- a/cs/Markdown/Tests/TokenParser_Tests.cs +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -10,12 +10,12 @@ 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); + // var act = () => Md.GenerateHtml(actualInput, actualTokens); + // var actualHtml = Md.GenerateHtml(actualInput, actualTokens); + // var expectedHtml = Md.GenerateHtml(actualInput, expectedTokens); - act.Should().NotThrow(); - expectedHtml.Should().Be(actualHtml); + // act.Should().NotThrow(); + // actualHtml.Should().Be(expectedHtml); actualTokens.Should().BeEquivalentTo(expectedTokens); } @@ -26,15 +26,15 @@ public static IEnumerable ParseSimpleText_Source() "__main title__\n__some bold text__", new [] { - new Token(Marks.Bold, "main title", 0, 14), - new Token(Marks.Bold, "some bold text", 15, 33) + new Token(Marks.Bold, "main title", 0, 12), + new Token(Marks.Bold, "some bold text", 15, 31) }).SetName("Bold text"); yield return new TestCaseData( "_main title_\n_some italic text_", new [] { - new Token(Marks.Italic, "main title", 0, 12), - new Token(Marks.Italic, "some italic text", 13, 31) + new Token(Marks.Italic, "main title", 0, 11), + new Token(Marks.Italic, "some italic text", 13, 30) }).SetName("Italic text"); yield return new TestCaseData( "# main title\n# some header text", @@ -43,22 +43,31 @@ public static IEnumerable ParseSimpleText_Source() new Token(Marks.Header, "main title", 0, 12), new Token(Marks.Header, "some header text", 13, 31) }).SetName("Headers text"); + yield return new TestCaseData( + "- main title\n- some bold text", + new [] + { + new Token(Marks.List, "main title", 0, 12), + new Token(Marks.List, "some bold text", 13, 29) + }).SetName("List text"); yield return new TestCaseData( "- __bold__\n- _italic_\n- # header", new [] { - new Token(Marks.List, "bold", 0, 10), - new Token(Marks.Bold, "bold", 2, 10), - new Token(Marks.List, "italic", 11, 21), - new Token(Marks.Italic, "italic", 13, 21), - new Token(Marks.List, "

    header

    ", 22, 32) + new Token(Marks.List, "__bold__", 0, 10), + new Token(Marks.Bold, "bold", 2, 8), + new Token(Marks.List, "_italic_", 11, 21), + new Token(Marks.Italic, "italic", 13, 20), + new Token(Marks.List, "# header", 22, 32), + new Token(Marks.Header, "header", 24, 32) } ).SetName("List with tags inside"); yield return new TestCaseData( "# - 123\n", new [] { - new Token(Marks.Header, "
  • 123
  • ", 0, 8), + new Token(Marks.Header, "- 123", 0, 7), + new Token(Marks.List, "123", 2, 7), } ).SetName("List inside header"); } diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index a7fd8a2a7..3ad7d021f 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -5,27 +5,27 @@ namespace Markdown; public class Token { - private string value; private Tag tag; public string Head => tag.OpenTag; public string Tail => tag.CloseTag; - public int Length => value.Length; - public int StartPosition { get; } public int EndPosition { get; } + public int SpaceAfterTag => Marks.AfterMarkSpace(Marks.GetMarkByTagName(tag.Name)); + public int MarkLength => Marks.GetMarkByTagName(tag.Name).Length; - public int OriginalLength => EndPosition - StartPosition; + public int PushForward => 2; + public string Content { get; } - public Token(Tag tag, string value, int start, int end) + public Token(Tag tag, string content, int start, int end) { this.tag = tag; - this.value = value; - this.StartPosition = start; - this.EndPosition = end; + Content = content; + StartPosition = start; + EndPosition = end; } - public Token(string mark, string value, int start, int end) : this(TagFactory.BuildTag(mark), value, start, end) + public Token(string mark, string content, int start, int end) : this(TagFactory.BuildTag(mark), content, start, end) { } @@ -33,7 +33,7 @@ public override string ToString() { var builder = new StringBuilder(); builder.Append(Head); - builder.Append(value); + builder.Append(Content); builder.Append(Tail); return builder.ToString(); } diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs index d1c5516ab..ea6786225 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -69,27 +69,15 @@ private IEnumerable FindAllTokens() } else { - // tags different -> closing = cross - if (!findedTag.IsOpening && !findedTag.IsOpenClose) + if (lastTag.Mark != Marks.Italic) { _tagStack.Push(lastTag); _tagStack.Push(findedTag); } - else - { - // check if possible incapsulate - var currentMark = findedTag.Mark; - var lastMark = lastTag.Mark; - if (lastMark != Marks.Italic && lastMark != currentMark) - { - _tagStack.Push(findedTag); - } - } } } else PushIfOpened(findedTag); - i++; } foreach (var token in BuildTokensFromStack(_text.Length)) { @@ -104,7 +92,7 @@ private IEnumerable BuildTokensFromStack(int lineEnd) var current = _tagStack.Pop(); if (current.Mark == Marks.Header || current.Mark == Marks.List) { - var token = BuildTokenOrNull(current, new PositionedTag(lineEnd+1, current.Mark, false)); + var token = BuildTokenOrNull(current, new PositionedTag(lineEnd, current.Mark, false)); if (token is not null) yield return token; } @@ -193,7 +181,7 @@ private bool CheckMarkForList(int index, bool isOpening) { return isOpening && index + 1 < _text.Length - && _text[index].ToString() == Marks.Header + && _text[index].ToString() == Marks.List && char.IsWhiteSpace(_text[index+1]); } @@ -203,11 +191,11 @@ private bool CheckMarkForList(int index, bool isOpening) { var mark = startTag.Mark; var start = startTag.Position; - var end = endTag.Position + mark.Length - 2 * Marks.AfterMarkSpace(mark); + var end = endTag.Position; - var content = _text.Substring(start + mark.Length + Marks.AfterMarkSpace(mark), endTag.Position - start - mark.Length - 2 * Marks.AfterMarkSpace(mark)); + var content = _text.Substring(start + mark.Length + Marks.AfterMarkSpace(mark), endTag.Position - start - mark.Length - Marks.AfterMarkSpace(mark)); - if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) + if (!_validator.IsContentAcceptable(content, mark) || _validator.IsSplittingWords(start, end + mark.Length)) { return null; } From 307172403129dba7135691746f821234f2333480 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Wed, 12 Nov 2025 19:32:37 +0500 Subject: [PATCH 10/12] fixed --- cs/Markdown/Md.cs | 30 +++++++++++++------------- cs/Markdown/Tests/Markdown_Tests.cs | 7 +++--- cs/Markdown/Tests/TokenParser_Tests.cs | 22 +++++++++---------- cs/Markdown/Token.cs | 17 ++++++++++++--- cs/Markdown/TokenParser.cs | 2 ++ 5 files changed, 46 insertions(+), 32 deletions(-) diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 30ac8d5c8..e79f44bfc 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -21,30 +21,30 @@ public static string GenerateHtml(string text, IEnumerable tokens) var outerEnd = text.Length + 3; foreach (var token in tokens) { - if (token.EndPosition > outerEnd) + if (token.StartPosition >= outerEnd) { - mergedText.Append(text.Substring(prevPosition,token.EndPosition - prevPosition - 2)); - mergedText.Append(token.Tail); - prevPosition = token.EndPosition; - } - else - { - mergedText.Append(text.Substring(prevPosition,token.StartPosition-prevPosition)); - mergedText.Append(token.Head); - prevPosition = token.StartPosition + 2; - outerEnd = token.EndPosition; - tokenStack.Push(token); + 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.MarkLength,token.EndPosition - prevPosition)); + mergedText.Append(text.Substring(prevPosition,token.EndPosition - prevPosition)); mergedText.Append(token.Tail); - prevPosition = token.EndPosition + 2; + prevPosition = token.EndPosition + token.Gap(false); } - prevPosition -= 3; mergedText.Append(text.Substring(prevPosition, text.Length - prevPosition)); return mergedText.ToString(); } diff --git a/cs/Markdown/Tests/Markdown_Tests.cs b/cs/Markdown/Tests/Markdown_Tests.cs index 100920998..6d128be97 100644 --- a/cs/Markdown/Tests/Markdown_Tests.cs +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -16,7 +16,7 @@ public static IEnumerable GenerateHtmlSource() new [] { new Token(Marks.Header, "main title", 0, 12), - new Token(Marks.Bold, "some bold text", 13, 31) + new Token(Marks.Bold, "some bold text", 13, 29) } ).SetName("Simple text"); yield return new TestCaseData( @@ -25,7 +25,8 @@ public static IEnumerable GenerateHtmlSource() new [] { new Token(Marks.Header, "main title", 0, 12), - new Token(Marks.Bold, "some bold text", 13, 33) + new Token(Marks.Bold, "some _bold_ text", 13, 31), + new Token(Marks.Italic, "bold", 20, 25) } ).SetName("Token inside token"); } @@ -52,7 +53,7 @@ public static IEnumerable Render_Source() ).SetName("Simple list"); yield return new TestCaseData( "# Заголовок с# заголовком\n\n _нач_ало се__реди__на ко_нец_ а __также _вложенные_ тэги __сам_ых__ раз_ных__ видов__", - "

    Заголовок с# заголовком

    \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\\

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

    ", diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs index 8fc780119..6cda49369 100644 --- a/cs/Markdown/Tests/TokenParser_Tests.cs +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -10,12 +10,12 @@ 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); + 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); + act.Should().NotThrow(); + actualHtml.Should().Be(expectedHtml); actualTokens.Should().BeEquivalentTo(expectedTokens); } @@ -84,17 +84,17 @@ public static IEnumerable ParseNestingText_Source() "# __main title__\n__some bold text__", new [] { - new Token(Marks.Header, "main title", 0, 16), - new Token(Marks.Bold, "main title", 2, 16), - new Token(Marks.Bold, "some bold text", 17, 35) + new Token(Marks.Header, "__main title__", 0, 16), + new Token(Marks.Bold, "main title", 2, 14), + new Token(Marks.Bold, "some bold text", 17, 33) }).SetName("Inside header"); yield return new TestCaseData( "# __main title__\n# __some _bold_ text__", new [] { - new Token(Marks.Header, "main title", 0, 16), - new Token(Marks.Bold, "main title", 2, 16), - new Token(Marks.Header, "some bold text", 17, 39), + new Token(Marks.Header, "__main title__", 0, 16), + new Token(Marks.Bold, "main title", 2, 14), + new Token(Marks.Header, "__some _bold_ text__", 17, 39), new Token(Marks.Bold, "some bold text", 19, 39), new Token(Marks.Italic, "bold", 26, 32) }).SetName("Italic inside Bold"); diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 3ad7d021f..04b8abcbf 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -6,15 +6,26 @@ namespace Markdown; 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 SpaceAfterTag => Marks.AfterMarkSpace(Marks.GetMarkByTagName(tag.Name)); - public int MarkLength => Marks.GetMarkByTagName(tag.Name).Length; + public int MarkLength => mark.Length; - public int PushForward => 2; + public int Gap(bool isOpened) + { + if (mark == Marks.Header || mark == Marks.List) + { + if (isOpened) + { + return MarkLength + 1; + } + return 0; + } + return MarkLength; + } public string Content { get; } public Token(Tag tag, string content, int start, int end) diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs index ea6786225..bf0c9b711 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -65,6 +65,8 @@ private IEnumerable FindAllTokens() } else // two open or close in a row -> use second opener and first closer + if (lastTag.Name == TagNames.Header || lastTag.Name == TagNames.List) + _tagStack.Push(lastTag); PushIfOpened(findedTag); } else From 2a1c0ff7c109c54868d2583545f9ea397c525920 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Thu, 13 Nov 2025 02:29:52 +0500 Subject: [PATCH 11/12] success --- cs/Markdown/{ => Data}/Marks.cs | 4 +- cs/Markdown/{ => Data}/PositionedTag.cs | 2 +- cs/Markdown/{ => Data}/Tag.cs | 4 +- cs/Markdown/{ => Data}/TagFactory.cs | 18 +- cs/Markdown/Data/Token.cs | 35 +++ cs/Markdown/Md.cs | 22 +- cs/Markdown/ParserValidator.cs | 21 +- cs/Markdown/Tests/Markdown_Tests.cs | 40 ++-- cs/Markdown/Tests/Tags_Tests.cs | 1 + cs/Markdown/Tests/TokenParser_Tests.cs | 88 ++++---- cs/Markdown/Token.cs | 51 ----- cs/Markdown/TokenParser.cs | 271 ++++++------------------ 12 files changed, 193 insertions(+), 364 deletions(-) rename cs/Markdown/{ => Data}/Marks.cs (90%) rename cs/Markdown/{ => Data}/PositionedTag.cs (94%) rename cs/Markdown/{ => Data}/Tag.cs (79%) rename cs/Markdown/{ => Data}/TagFactory.cs (59%) create mode 100644 cs/Markdown/Data/Token.cs delete mode 100644 cs/Markdown/Token.cs diff --git a/cs/Markdown/Marks.cs b/cs/Markdown/Data/Marks.cs similarity index 90% rename from cs/Markdown/Marks.cs rename to cs/Markdown/Data/Marks.cs index fc892d015..31dd73b52 100644 --- a/cs/Markdown/Marks.cs +++ b/cs/Markdown/Data/Marks.cs @@ -1,4 +1,4 @@ -namespace Markdown; +namespace Markdown.Data; public static class Marks { @@ -7,7 +7,7 @@ public static class Marks public const string Header = "#"; public const string List = "-"; - public static IEnumerable AllMarks = new[] + public static readonly IEnumerable AllMarks = new[] { Bold, Italic, diff --git a/cs/Markdown/PositionedTag.cs b/cs/Markdown/Data/PositionedTag.cs similarity index 94% rename from cs/Markdown/PositionedTag.cs rename to cs/Markdown/Data/PositionedTag.cs index c1cad32dd..a8d9add2f 100644 --- a/cs/Markdown/PositionedTag.cs +++ b/cs/Markdown/Data/PositionedTag.cs @@ -1,4 +1,4 @@ -namespace Markdown; +namespace Markdown.Data; public class PositionedTag : Tag { diff --git a/cs/Markdown/Tag.cs b/cs/Markdown/Data/Tag.cs similarity index 79% rename from cs/Markdown/Tag.cs rename to cs/Markdown/Data/Tag.cs index 8083aede0..7c98f2664 100644 --- a/cs/Markdown/Tag.cs +++ b/cs/Markdown/Data/Tag.cs @@ -1,4 +1,4 @@ -namespace Markdown; +namespace Markdown.Data; public class Tag { @@ -8,6 +8,6 @@ public class Tag public Tag(string name) { - this.Name = name; + Name = name; } } \ No newline at end of file diff --git a/cs/Markdown/TagFactory.cs b/cs/Markdown/Data/TagFactory.cs similarity index 59% rename from cs/Markdown/TagFactory.cs rename to cs/Markdown/Data/TagFactory.cs index 7f3c9f3b5..adebff08f 100644 --- a/cs/Markdown/TagFactory.cs +++ b/cs/Markdown/Data/TagFactory.cs @@ -1,4 +1,4 @@ -namespace Markdown; +namespace Markdown.Data; public static class TagNames { public const string Strong = "strong"; @@ -9,23 +9,23 @@ public static class TagNames { public static class TagFactory { - public static Tag Bold => new(TagNames.Strong); - public static Tag Italic => new(TagNames.Em); - public static Tag Header => new(TagNames.Header); - public static Tag List => new(TagNames.List); + 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; + return _header; case Marks.Bold: - return Bold; + return _bold; case Marks.Italic: - return Italic; + return _italic; case Marks.List: - return List; + return _list; default: throw new ArgumentException("Wrong mark!"); } 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/Md.cs b/cs/Markdown/Md.cs index e79f44bfc..8cac7d5ec 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -1,4 +1,5 @@ using System.Text; +using Markdown.Data; namespace Markdown; @@ -48,27 +49,6 @@ public static string GenerateHtml(string text, IEnumerable tokens) mergedText.Append(text.Substring(prevPosition, text.Length - prevPosition)); return mergedText.ToString(); } - - public static string GenerateHtmlOld(string text, IEnumerable tokens) - { - var mergedText = new StringBuilder(); - var prevPosition = 0; - var nextPositionMin = -1; - foreach (var token in tokens) - { - var currentPosition = token.StartPosition; - if (currentPosition < nextPositionMin) - { - continue; - } - mergedText.Append(text.Substring(prevPosition,currentPosition-prevPosition)); - mergedText.Append(token.ToString()); - prevPosition = token.EndPosition; - nextPositionMin = token.EndPosition; - } - mergedText.Append(text.Substring(prevPosition)); - return mergedText.ToString(); - } } diff --git a/cs/Markdown/ParserValidator.cs b/cs/Markdown/ParserValidator.cs index 64b34cf80..96b02950d 100644 --- a/cs/Markdown/ParserValidator.cs +++ b/cs/Markdown/ParserValidator.cs @@ -1,4 +1,5 @@ namespace Markdown; +using Markdown.Data; public class ParserValidator { @@ -11,20 +12,22 @@ public ParserValidator(string input) public bool IsMarkCorrect(int startIndex, bool isOpening, int markLength = 1) { - var isScreened = (startIndex > 0 && _text[startIndex - 1] == '\\') - && (startIndex > 1 && _text[startIndex - 2] != '\\' || startIndex == 1); + var isScreened = IsScreened(startIndex); if (isOpening) { return !isScreened && startIndex + markLength < _text.Length && _text[startIndex + markLength] != ' '; } - else - { - return !isScreened - && startIndex > 0 - && _text[startIndex - 1] != ' '; - } + return !isScreened + && startIndex > 0 + && _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) @@ -46,7 +49,7 @@ public bool IsSplittingWords(int start, int end) && _text[end] != '\n'; } - public bool HasNoDigits(string content) + private bool HasNoDigits(string content) { foreach (char c in content) { diff --git a/cs/Markdown/Tests/Markdown_Tests.cs b/cs/Markdown/Tests/Markdown_Tests.cs index 6d128be97..1c556d42f 100644 --- a/cs/Markdown/Tests/Markdown_Tests.cs +++ b/cs/Markdown/Tests/Markdown_Tests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using NUnit.Framework; using System.Text; +using Markdown.Data; namespace Markdown.Tests; @@ -15,8 +16,8 @@ public static IEnumerable GenerateHtmlSource() "

    main title

    \nsome bold text", new [] { - new Token(Marks.Header, "main title", 0, 12), - new Token(Marks.Bold, "some bold text", 13, 29) + new Token(Marks.Header, 0, 12), + new Token(Marks.Bold, 13, 29) } ).SetName("Simple text"); yield return new TestCaseData( @@ -24,9 +25,9 @@ public static IEnumerable GenerateHtmlSource() "

    main title

    \nsome bold text", new [] { - new Token(Marks.Header, "main title", 0, 12), - new Token(Marks.Bold, "some _bold_ text", 13, 31), - new Token(Marks.Italic, "bold", 20, 25) + new Token(Marks.Header, 0, 12), + new Token(Marks.Bold, 13, 31), + new Token(Marks.Italic, 20, 25) } ).SetName("Token inside token"); } @@ -53,11 +54,11 @@ public static IEnumerable Render_Source() ).SetName("Simple list"); yield return new TestCaseData( "# Заголовок с# заголовком\n\n _нач_ало се__реди__на ко_нец_ а __также _вложенные_ тэги __сам_ых__ раз_ных__ видов__", - "

    Заголовок с

    заголовком

    \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\\

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

    " + "# Спецификация языка разметки\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"); } @@ -67,25 +68,30 @@ public void Render_DifferentText(string actual, string expected) Md.Render(actual).Should().Be(expected); } - [Test] - public void EffiecencyTest() + [Test, Explicit] + [TestCase(10000, 10)] + [TestCase(10000, 1000)] + public void ЕfficiencyTest(long n, long coefficient) { + var input = StackString(n); var stopwatch = Stopwatch.StartNew(); - Md.Render(StackString(10000)); + var res = Md.Render(input); stopwatch.Stop(); - var time1 = stopwatch.ElapsedMilliseconds * 10 - 1000; - - stopwatch.Restart(); - Md.Render(StackString(100000)); + 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(int times) + private string StackString(long times) { var sb = new StringBuilder(); - for (int i = 0; i < 10000; i++) + for (int i = 0; i < times; i++) { sb.Append("__a"); } diff --git a/cs/Markdown/Tests/Tags_Tests.cs b/cs/Markdown/Tests/Tags_Tests.cs index ba91756aa..c5fbf0609 100644 --- a/cs/Markdown/Tests/Tags_Tests.cs +++ b/cs/Markdown/Tests/Tags_Tests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using Markdown.Data; namespace Markdown.Tests; diff --git a/cs/Markdown/Tests/TokenParser_Tests.cs b/cs/Markdown/Tests/TokenParser_Tests.cs index 6cda49369..514e2c172 100644 --- a/cs/Markdown/Tests/TokenParser_Tests.cs +++ b/cs/Markdown/Tests/TokenParser_Tests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using Markdown.Data; namespace Markdown.Tests; @@ -24,50 +25,50 @@ public static IEnumerable ParseSimpleText_Source() { yield return new TestCaseData( "__main title__\n__some bold text__", - new [] + new Token[] { - new Token(Marks.Bold, "main title", 0, 12), - new Token(Marks.Bold, "some bold text", 15, 31) + new (Marks.Bold, 0, 12), + new (Marks.Bold, 15, 31) }).SetName("Bold text"); yield return new TestCaseData( "_main title_\n_some italic text_", - new [] + new Token[] { - new Token(Marks.Italic, "main title", 0, 11), - new Token(Marks.Italic, "some italic text", 13, 30) + new (Marks.Italic, 0, 11), + new (Marks.Italic, 13, 30) }).SetName("Italic text"); yield return new TestCaseData( "# main title\n# some header text", - new [] + new Token[] { - new Token(Marks.Header, "main title", 0, 12), - new Token(Marks.Header, "some header text", 13, 31) + new (Marks.Header, 0, 12), + new (Marks.Header, 13, 31) }).SetName("Headers text"); yield return new TestCaseData( "- main title\n- some bold text", - new [] + new Token[] { - new Token(Marks.List, "main title", 0, 12), - new Token(Marks.List, "some bold text", 13, 29) + new (Marks.List, 0, 12), + new (Marks.List, 13, 29) }).SetName("List text"); yield return new TestCaseData( "- __bold__\n- _italic_\n- # header", - new [] + new Token[] { - new Token(Marks.List, "__bold__", 0, 10), - new Token(Marks.Bold, "bold", 2, 8), - new Token(Marks.List, "_italic_", 11, 21), - new Token(Marks.Italic, "italic", 13, 20), - new Token(Marks.List, "# header", 22, 32), - new Token(Marks.Header, "header", 24, 32) + 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 [] + new Token[] { - new Token(Marks.Header, "- 123", 0, 7), - new Token(Marks.List, "123", 2, 7), + new (Marks.Header, 0, 7), + new (Marks.List, 2, 7), } ).SetName("List inside header"); } @@ -82,21 +83,21 @@ public static IEnumerable ParseNestingText_Source() { yield return new TestCaseData( "# __main title__\n__some bold text__", - new [] + new Token[] { - new Token(Marks.Header, "__main title__", 0, 16), - new Token(Marks.Bold, "main title", 2, 14), - new Token(Marks.Bold, "some bold text", 17, 33) + 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 [] + new Token[] { - new Token(Marks.Header, "__main title__", 0, 16), - new Token(Marks.Bold, "main title", 2, 14), - new Token(Marks.Header, "__some _bold_ text__", 17, 39), - new Token(Marks.Bold, "some bold text", 19, 39), - new Token(Marks.Italic, "bold", 26, 32) + 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"); } @@ -111,10 +112,9 @@ public static IEnumerable ParseTextWithExceptions_Source() { yield return new TestCaseData( "внутри _одинарного __двойное__ не_ работает", - new [] + new Token[] { - new Token(Marks.Italic, "одинарного __двойное__ не", 7, 34), - new Token(Marks.Bold, "двойное", 19, 30) + new (Marks.Italic, 7, 33) }).SetName("Bold inside Italic"); yield return new TestCaseData( "c цифрами_12_3 не считаются выделением __даже1так__", @@ -124,9 +124,9 @@ public static IEnumerable ParseTextWithExceptions_Source() "_нач_ало се_ред_ина ко_нец_", new Token[] { - new Token(Marks.Italic, "нач", 0, 5), - new Token(Marks.Italic, "ред", 11, 16), - new Token(Marks.Italic, "нец", 22, 27), + new (Marks.Italic, 0, 4), + new (Marks.Italic, 11, 15), + new (Marks.Italic, 22, 26), } ).SetName("Parts of words"); yield return new TestCaseData( @@ -150,8 +150,16 @@ public static IEnumerable ParseTextWithExceptions_Source() 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( "_пересечения __двойных_ и _одинарных__ подчерков_", @@ -161,7 +169,7 @@ public static IEnumerable ParseTextWithExceptions_Source() @"экран\_ирование\_ и \\__двойное\\__ экрани\рование", new Token[] { - new Token(Marks.Bold, @"двойное\\", 22, 35) + new (Marks.Bold,22, 33) } ).SetName("Shielding marks"); } diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs deleted file mode 100644 index 04b8abcbf..000000000 --- a/cs/Markdown/Token.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Text; -using NUnit.Framework; - -namespace Markdown; - -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 MarkLength => mark.Length; - - public int Gap(bool isOpened) - { - if (mark == Marks.Header || mark == Marks.List) - { - if (isOpened) - { - return MarkLength + 1; - } - return 0; - } - return MarkLength; - } - public string Content { get; } - - public Token(Tag tag, string content, int start, int end) - { - this.tag = tag; - Content = content; - StartPosition = start; - EndPosition = end; - } - - public Token(string mark, string content, int start, int end) : this(TagFactory.BuildTag(mark), content, start, end) - { - } - - public override string ToString() - { - var builder = new StringBuilder(); - builder.Append(Head); - builder.Append(Content); - builder.Append(Tail); - return builder.ToString(); - } -} \ No newline at end of file diff --git a/cs/Markdown/TokenParser.cs b/cs/Markdown/TokenParser.cs index bf0c9b711..d3c0dc461 100644 --- a/cs/Markdown/TokenParser.cs +++ b/cs/Markdown/TokenParser.cs @@ -1,22 +1,20 @@ -using System.Collections; +using Markdown.Data; namespace Markdown; public class TokenParser { private List _tokens; - private HashSet _allFindedTokens; private string _text; private ParserValidator _validator; - private Stack _tagStack; + private Stack _stackOfTags; public TokenParser(string text = "") { _text = text; _validator = new (text); _tokens = new (); - _allFindedTokens = new (); - _tagStack = new (); + _stackOfTags = new (); } public IEnumerable ParseTokens(string input, string outerTokenMark = "") @@ -24,8 +22,7 @@ public IEnumerable ParseTokens(string input, string outerTokenMark = "") _text = input; _validator = new ParserValidator(input); _tokens = new List(); - _allFindedTokens = new HashSet(); - _tagStack = new Stack(); + _stackOfTags = new Stack(); _tokens.AddRange(FindAllTokens()); @@ -50,13 +47,14 @@ private IEnumerable FindAllTokens() if (findedTag == null) continue; - // что-то уже лежит - if (_tagStack.Count > 0) + if (_stackOfTags.Count > 0) { - var lastTag = _tagStack.Pop(); + var lastTag = _stackOfTags.Pop(); if (lastTag.Name == findedTag.Name) { - // types equal check if open && close + if (CheckOuterToken()) + continue; + if (lastTag.IsOpening && (!findedTag.IsOpening || findedTag.IsOpenClose)) { var token = BuildTokenOrNull(lastTag, findedTag); @@ -64,18 +62,17 @@ private IEnumerable FindAllTokens() yield return token; } else - // two open or close in a row -> use second opener and first closer + { if (lastTag.Name == TagNames.Header || lastTag.Name == TagNames.List) - _tagStack.Push(lastTag); + _stackOfTags.Push(lastTag); PushIfOpened(findedTag); + } } else { - if (lastTag.Mark != Marks.Italic) - { - _tagStack.Push(lastTag); - _tagStack.Push(findedTag); - } + + _stackOfTags.Push(lastTag); + _stackOfTags.Push(findedTag); } } else @@ -87,25 +84,61 @@ private IEnumerable FindAllTokens() } } + 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 (_tagStack.Count > 0) + while (_stackOfTags.Count > 0) { - var current = _tagStack.Pop(); + 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) - _tagStack.Push(findedTag); + _stackOfTags.Push(findedTag); } private PositionedTag? FindTag(int index) @@ -176,7 +209,8 @@ private bool CheckMarkForHeader(int index, bool isOpening) return isOpening && index + 1 < _text.Length && _text[index].ToString() == Marks.Header - && char.IsWhiteSpace(_text[index + 1]); + && char.IsWhiteSpace(_text[index + 1]) + && !_validator.IsScreened(index); } private bool CheckMarkForList(int index, bool isOpening) @@ -184,196 +218,9 @@ private bool CheckMarkForList(int index, bool isOpening) return isOpening && index + 1 < _text.Length && _text[index].ToString() == Marks.List - && char.IsWhiteSpace(_text[index+1]); - } - - #endregion - - private Token? BuildTokenOrNull(PositionedTag startTag, PositionedTag endTag) - { - var mark = startTag.Mark; - var start = startTag.Position; - var end = endTag.Position; - - 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), - content, - start, - end - ); - - // возможно рудимент - _allFindedTokens.Add(token); - if (!SolveOverllaping(start, end)) - return token; - return null; - } - - private bool SolveOverllaping(int start, int end) - { - var isOverlapping = false; - foreach (var token in _allFindedTokens) - { - if ( - end > token.EndPosition - && token.StartPosition < start && start < token.EndPosition - || token.StartPosition < end && end < token.EndPosition - && start < token.StartPosition - ) - { - _tokens.Remove(token); - isOverlapping = true; - } - } - return isOverlapping; - } - - # region commented - /* - private void AddHeaderTokens(string outerTokenMark = "") - { - _tokens.AddRange(FindParagraphTokens(Marks.Header)); - } - - private void AddListTokens(string outerTokenMark = "") - { - _tokens.AddRange(FindParagraphTokens(Marks.List)); - } - - private void AddBoldTokens(string outerTokenMark) - { - if (outerTokenMark != Marks.Italic && outerTokenMark != Marks.Bold) - { - _tokens.AddRange(FindTokens( - (i, isOpeningTag) => _text[i] == '_' && _text[i+1] == '_' && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Bold.Length), - Marks.Bold - )); - } - } - private void AddItalicTokens(string outerTokenMark) - { - if (outerTokenMark != Marks.Italic) - { - _tokens.AddRange(FindTokens( - (i, isOpeningTag) => _text[i] == '_' && !_validator.IsDoubleUnderscore(i) && _validator.IsMarkCorrect(i, isOpeningTag, Marks.Italic.Length), - Marks.Italic - )); - } - } - - - private IEnumerable FindTokens(Func checkMark, string mark) - { - var stack = new Stack(); - - for (int i = 0; i < _text.Length+1 - mark.Length; i++) - { - var isOpeningTag = stack.Count == 0; - - if (checkMark(i, isOpeningTag)) - { - if (stack.Count > 0) - { - var start = stack.Pop(); - var end = i + mark.Length; - - var content = _text.Substring(start + mark.Length, i - start - mark.Length); - - if (!_validator.IsContentAcceptable(content) || _validator.IsSplittingWords(start, end)) - { - continue; - } - var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); - var token = new Token( - TagFactory.BuildTag(mark), - htmlContent, - start, - end - ); - - _allFindedTokens.Add(token); - if (SolveOverllaping(start, end)) - yield return token; - } - else - { - stack.Push(i); - } - i++; - } - } + && char.IsWhiteSpace(_text[index+1]) + && !_validator.IsScreened(index); } - private IEnumerable FindParagraphTokens(string mark) - { - // Обрабатываем текст построчно для заголовков - int lineStart = 0; - - for (int i = 0; i < _text.Length; i++) - { - if (_text[i] == '\n' || i == _text.Length - 1) - { - // Определяем конец строки - int lineEnd = (i == _text.Length - 1) ? i + 1 : i; - foreach (var token in ParseLineToTokens(lineStart, lineEnd, mark)) - yield return token; - - lineStart = i + 1; - } - } - - // Обрабатываем последнюю строку, если текст не заканчивается \n - foreach (var token in ParseLineToTokens(lineStart, _text.Length, mark)) - yield return token; - } - - private IEnumerable ParseLineToTokens(int lineStart, int lineEnd, string mark) - { - int lineLength = lineEnd - lineStart; - - if (lineLength > 0) - { - foreach (var token in FindTokensInLine(lineStart, lineEnd, mark)) - { - _allFindedTokens.Add(token); - if (SolveOverllaping(token.StartPosition, token.EndPosition)) - yield return token; - } - } - } - - private IEnumerable FindTokensInLine(int lineStart, int lineEnd, string mark) - { - // Если есть # и пробел - int pos = lineStart; - while (pos < lineEnd - 1 && _text[pos].ToString() != mark && char.IsWhiteSpace(_text[pos+1])) - { - pos++; - } - - if (pos < lineEnd && _text[pos].ToString() == mark) - { - // Пропускаем пробел после # - pos++; - - string content = _text.Substring(pos, lineEnd - pos).Trim(); - var htmlContent = Md.GenerateHtml(content, new TokenParser().ParseTokens(content, mark)); - - yield return new Token( - TagFactory.BuildTag(mark), - htmlContent, - pos - 1, - lineEnd - ); - } - } - */ #endregion } \ No newline at end of file From 9ab1b7da2ff2f60c081fc0bedf2a742e2a378865 Mon Sep 17 00:00:00 2001 From: marchenko <1maks_2055@mail.ru> Date: Thu, 13 Nov 2025 02:38:09 +0500 Subject: [PATCH 12/12] some sugar --- cs/Markdown/ParserValidator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cs/Markdown/ParserValidator.cs b/cs/Markdown/ParserValidator.cs index 96b02950d..51c6c4303 100644 --- a/cs/Markdown/ParserValidator.cs +++ b/cs/Markdown/ParserValidator.cs @@ -17,11 +17,11 @@ public bool IsMarkCorrect(int startIndex, bool isOpening, int markLength = 1) { return !isScreened && startIndex + markLength < _text.Length - && _text[startIndex + markLength] != ' '; + && !Char.IsWhiteSpace(_text[startIndex + markLength]); } return !isScreened && startIndex > 0 - && _text[startIndex - 1] != ' '; + && !Char.IsWhiteSpace(_text[startIndex - 1]); } public bool IsScreened(int index) @@ -45,7 +45,7 @@ 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).Contains(' ') + && _text.Substring(start, end - start).Any(Char.IsWhiteSpace) && _text[end] != '\n'; }