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 => $"{Name}>";
+
+ 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 => $"{Name}>";
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\nitalic\nheader
"
+ ).SetName("Simple list");
+ yield return new TestCaseData(
+ "# Заголовок с# заголовком\n\n _нач_ало се__реди__на ко_нец_ а __также _вложенные_ тэги __сам_ых__ раз_ных__ видов__",
+ "Заголовок с# заголовком
\n\n начало середина конец а также вложенные тэги __сам_ых раз_ных__ видов__"
+ ).SetName("Normal text");
+ yield return new TestCaseData(
+ "# Спецификация языка разметки\n\nПосмотрите этот файл в сыром виде. Сравните с тем, что показывает github.\nВсе совпадения случайны ;)\n\n\n\n# Курсив\n\nТекст, _окруженный с двух сторон_ одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\ вот так:\n\nТекст, \\окруженный с двух сторон\\ одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\.\n\n\n\n# Полужирный\n\n__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега \\.\n\n\n\n# Экранирование\n\nЛюбой символ можно экранировать, чтобы он не считался частью разметки.\n\\_Вот это\\_, не должно выделиться тегом \\.\n\nСимвол экранирования исчезает из результата, только если экранирует что-то.\nЗдесь сим\\волы экранирования\\ \\должны остаться.\\\n\nСимвол экранирования тоже можно экранировать: \\\\_вот это будет выделено тегом_ \\\n\n\n\n# Взаимодействие тегов\n\nВнутри __двойного выделения _одинарное_ тоже__ работает.\n\nНо не наоборот — внутри _одинарного __двойное__ не_ работает.\n\nПодчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка.\n\nОднако выделять часть слова они могут: и в _нач_але, и в сер_еди_не, и в кон_це._\n\nВ то же время выделение в ра_зных сл_овах не работает.\n\n__Непарные_ символы в рамках одного абзаца не считаются выделением.\n\nЗа подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением \nи остаются просто символами подчерка.\n\nПодчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки _не считаются_ окончанием выделения \nи остаются просто символами подчерка.\n\nВ случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением.\n\nЕсли внутри подчерков пустая строка ____, то они остаются символами подчерка.\n\n\n\n# Заголовки\n\nАбзац, начинающийся с \"# \", выделяется тегом \\ в заголовок.\nВ тексте заголовка могут присутствовать все прочие символы разметки с указанными правилами.\n\nТаким образом\n\n# Заголовок __с _разными_ символами__\n\nпревратится в:\n\n\\Заголовок \\с \\разными\\ символами\\\\
",
+ "Спецификация языка разметки
\n\nПосмотрите этот файл в сыром виде. Сравните с тем, что показывает github.\nВсе совпадения случайны ;)\n\n\n\nКурсив
\n\nТекст, окруженный с двух сторон одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\ вот так:\n\nТекст, \\окруженный с двух сторон\\ одинарными символами подчерка,\nдолжен помещаться в HTML-тег \\.\n\n\n\nПолужирный
\n\n__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега \\.\n\n\n\nЭкранирование
\n\nЛюбой символ можно экранировать, чтобы он не считался частью разметки.\n\\_Вот это\\_, не должно выделиться тегом \\.\n\nСимвол экранирования исчезает из результата, только если экранирует что-то.\nЗдесь сим\\волы экранирования\\ \\должны остаться.\\\n\nСимвол экранирования тоже можно экранировать: \\\\_вот это будет выделено тегом_ \\\n\n\n\nВзаимодействие тегов
\n\nВнутри двойного выделения одинарное тоже работает.\n\nНо не наоборот — внутри одинарного __двойное__ не работает.\n\nПодчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка.\n\nОднако выделять часть слова они могут: и в начале, и в середине, и в конце.\n\nВ то же время выделение в ра_зных сл_овах не работает.\n\n__Непарные_ символы в рамках одного абзаца не считаются выделением.\n\nЗа подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением \nи остаются просто символами подчерка.\n\nПодчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти подчерки _не считаются окончанием выделения \nи остаются просто символами подчерка.\n\nВ случае __пересечения двойных__ и одинарных подчерков ни один из них не считается выделением.\n\nЕсли внутри подчерков пустая строка ____, то они остаются символами подчерка.\n\n\n\nЗаголовки
\n\nАбзац, начинающийся с \"# \", выделяется тегом \\ в заголовок.\nВ тексте заголовка могут присутствовать все прочие символы разметки с указанными правилами.\n\nТаким образом\n\nЗаголовок с разными символами
\n\nпревратится в:\n\n\\Заголовок \\с \\разными\\ символами\\\\
"
+ ).SetName("Biggest text");
+ }
+
+ [Test, TestCaseSource(nameof(Render_Source))]
+ public void Render_DifferentText(string actual, string expected)
+ {
+ Md.Render(actual).Should().Be(expected);
+ }
}
\ 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 => $"{Name}>";
- public class Tag
+ public Tag(string name)
{
- private string name;
- public string Name { get { return name; } }
- public string OpenTag => $"<{Name}>";
- public string CloseTag => $"{Name}>";
-
- 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';
}