diff --git a/cs/Markdown.Tests/Markdown.Tests.csproj b/cs/Markdown.Tests/Markdown.Tests.csproj
new file mode 100644
index 000000000..ba72b41b0
--- /dev/null
+++ b/cs/Markdown.Tests/Markdown.Tests.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cs/Markdown.Tests/MdTests.cs b/cs/Markdown.Tests/MdTests.cs
new file mode 100644
index 000000000..d18a89ccf
--- /dev/null
+++ b/cs/Markdown.Tests/MdTests.cs
@@ -0,0 +1,208 @@
+using FluentAssertions;
+using Markdown.Parsers.MdParsers;
+using Markdown.Renderers;
+using System.Diagnostics;
+
+namespace Markdown.Tests
+{
+ internal class MdTests
+ {
+ [Test]
+ public void Md_ThrowsException_ReceivingNullAsIParser()
+ {
+ Assert.Throws(() => new Md(null!, new RendererHTML()));
+ }
+
+ [Test]
+ public void Md_ThrowsException_ReceivingNullAsIRenderer()
+ {
+ Assert.Throws(() => new Md(new ParserMd(), null!));
+ }
+
+ [TestCase("__Bold token__", "Bold token")]
+ [TestCase("_Italic token_", "Italic token")]
+ [TestCase("# Header token", "Header token
")]
+ [TestCase("Text token", "Text token")]
+ [TestCase("# _Set_ __of__ tokens", "Set of tokens
")]
+ public void Md_RendersCorrectly_SimpleTokens(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase(@"\_\_Text token\_\_", "__Text token__")]
+ [TestCase(@"\_Text token\_", "_Text token_")]
+ [TestCase(@"\# Text token", "# Text token")]
+ [TestCase(@"#\ Text token", "# Text token")]
+ public void Md_RendersCorrectly_SimpleEscapedTokens(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase(@"Ste\gosaur\us", @"Ste\gosaur\us")]
+ [TestCase(@"\", @"\")]
+ [TestCase(@"_Italic \token_", @"Italic \token")]
+ public void Md_RendersCorrectly_WhenEscapedNothing(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase(@"\\a", @"\a")]
+ [TestCase(@"\\_\\_Text token\\_\\_", @"\\Text token\\")]
+ [TestCase(@"\\_Italic token\\_", @"\Italic token\")]
+ public void Md_RendersCorrectly_WhenEscapeEscaped(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("__Outer bold _Inner italic part_ outer bold__", "Outer bold Inner italic part outer bold")]
+ public void Md_RendersCorrectly_ItalicInsideBold(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("_Outer italic __Inner Bold part__ outer Italic_", "Outer italic __Inner Bold part__ outer Italic")]
+ public void Md_RendersCorrectly_BoldInsideItalic(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("_Digits 12 3_", "Digits 12 3")]
+ [TestCase("Digits_12_3", "Digits_12_3")]
+ [TestCase("Digits__12__3", "Digits__12__3")]
+ public void Md_RendersCorrectly_DigitsWithUnderscores(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+
+ [TestCase("__Sta__rt", "Start")]
+ [TestCase("_Sta_rt", "Start")]
+ [TestCase("S__tar__t", "Start")]
+ [TestCase("S_tar_t", "Start")]
+ [TestCase("St__art__", "Start")]
+ [TestCase("St_art_", "Start")]
+ public void Md_RendersCorrectly_WordsWithUnderscores(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("Hel_lo, Wor_ld", "Hel_lo, Wor_ld")]
+ [TestCase("Hel__lo, Wor__ld", "Hel__lo, Wor__ld")]
+ public void Md_RendersCorrectly_UnderscoresInsideDifferentWords(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("_Hello world__", "_Hello world__")]
+ [TestCase("__Hello world_", "__Hello world_")]
+ public void Md_RendersCorrectly_UnpairedTags(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("_ Hello world_", "_ Hello world_")]
+ [TestCase("_ Hello world _", "_ Hello world _")]
+ [TestCase("_Hello world _", "_Hello world _")]
+ [TestCase("__ Hello world__", "__ Hello world__")]
+ [TestCase("__ Hello world __", "__ Hello world __")]
+ [TestCase("__Hello world __", "__Hello world __")]
+ public void Md_RendersCorrectly_WithSpaceAfterOrBeforeTag(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("_Hello__ _world__", "_Hello__ _world__")]
+ [TestCase("__Hello_ __world_", "__Hello_ __world_")]
+ public void Md_RendersCorrectly_WithUnderscoreIntersections(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("__", "__")]
+ [TestCase("____", "____")]
+ public void Md_RendersCorrectly_UnderscoresWithEmptyValue(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase("# Header token 1\n# Header token 2", "Header token 1
Header token 2
")]
+ [TestCase("this is regular sentence # 123", "this is regular sentence # 123")]
+ public void Md_RendersCorrectly_Heading(string input, string expected)
+ {
+ var md = new Md(new ParserMd(), new RendererHTML());
+ var actual = md.Render(input);
+ actual.Should().Be(expected);
+ }
+
+ [TestCase(5, 10, 0.5)]
+ public void Md_ShouldWorkInLinearTime(int iterations, int baseIterationSize, double measurementError)
+ {
+ var testSizes = new int[iterations];
+ testSizes[0] = baseIterationSize;
+ for (var i = 1; i < iterations; i++)
+ {
+ testSizes[i] = 2 * testSizes[i - 1];
+ }
+
+ var executionTimes = GetExecutionTimes(testSizes);
+
+ for (var i = 1; i < iterations; i++)
+ {
+ var growthFactor = executionTimes[i] / executionTimes[i - 1];
+ Assert.That(growthFactor, Is.LessThanOrEqualTo(2.0 + measurementError));
+ }
+ }
+
+ private double[] GetExecutionTimes(int[] sizes)
+ {
+ var results = new double[sizes.Length];
+ var md = new Md(new ParserMd(), new RendererHTML());
+ md.Render(GenerateText(sizes[^1]));
+
+ for (var i = 0; i < sizes.Length; i++)
+ {
+ md = new Md(new ParserMd(), new RendererHTML());
+ var text = GenerateText(sizes[i]);
+
+ var stopwatch = Stopwatch.StartNew();
+ md.Render(text);
+ stopwatch.Stop();
+
+ results[i] = stopwatch.Elapsed.TotalMilliseconds;
+ }
+
+ return results;
+ }
+
+ private string GenerateText(int numberOfRepetitions)
+ => string.Concat(
+ Enumerable.Repeat(@"__This _is_ a__ simple text \_for\_ crea\ting complex _test_ __text__.",
+ numberOfRepetitions));
+ }
+}
diff --git a/cs/Markdown.Tests/ParsersTests/ParserMdTest.cs b/cs/Markdown.Tests/ParsersTests/ParserMdTest.cs
new file mode 100644
index 000000000..b158bdb53
--- /dev/null
+++ b/cs/Markdown.Tests/ParsersTests/ParserMdTest.cs
@@ -0,0 +1,77 @@
+using FluentAssertions;
+using Markdown.Parsers.MdParsers;
+using Markdown.Tokens.HtmlTokens;
+
+namespace Markdown.Tests.ParsersTests
+{
+ [TestFixture]
+ public class ParserMdTests
+ {
+ private ParserMd _parser;
+ private readonly List _expected = new List();
+
+ [SetUp]
+ public void SetUp()
+ {
+ _parser = new ParserMd();
+ _expected.Clear();
+ }
+
+ [TestCase("__Bold token__")]
+ public void Parse_SimpleBoldText_Correctly(string text)
+ {
+ _expected.Add(
+ new BoldToken(
+ new TextToken("Bold token")));
+ CheckCorrectness(text);
+ }
+
+ [TestCase("# Header token")]
+ public void Parse_SimpleHeaderText_Correctly(string text)
+ {
+ _expected.Add(
+ new HeaderToken(
+ new TextToken("Header token")));
+ CheckCorrectness(text);
+ }
+
+ [TestCase("_Italic token_")]
+ public void Parse_SimpleItalicText_Correctly(string text)
+ {
+ _expected.Add(
+ new ItalicToken(
+ new TextToken("Italic token")));
+ CheckCorrectness(text);
+ }
+
+ [TestCase("Text Token")]
+ public void Parse_SimpleText_Correctly(string text)
+ {
+ _expected.Add(
+ new TextToken(text));
+ CheckCorrectness(text);
+ }
+
+ [TestCase("# _Set_ __of__ tokens")]
+ public void Parse_ComplexText_Correctly(string text)
+ {
+ _expected.Add(
+ new HeaderToken(
+ new SetToken(
+ new ItalicToken(
+ new TextToken("Set")),
+ new TextToken(" "),
+ new BoldToken(
+ new TextToken("of")),
+ new TextToken(" tokens"))));
+ CheckCorrectness(text);
+ }
+
+ private void CheckCorrectness(string text)
+ {
+ var actual = _parser.Parse(text);
+ actual.Should().BeEquivalentTo(_expected,
+ options => options.RespectingRuntimeTypes());
+ }
+ }
+}
diff --git a/cs/Markdown.Tests/RenderesTests/RendererHtmlTests.cs b/cs/Markdown.Tests/RenderesTests/RendererHtmlTests.cs
new file mode 100644
index 000000000..9305b096a
--- /dev/null
+++ b/cs/Markdown.Tests/RenderesTests/RendererHtmlTests.cs
@@ -0,0 +1,115 @@
+using FluentAssertions;
+using Markdown.Renderers;
+using Markdown.Tokens.HtmlTokens;
+
+namespace Markdown.Tests.RenderesTests
+{
+ [TestFixture]
+ public class RendererHtmlTests
+ {
+ private RendererHTML _renderer;
+ private readonly string _dummyText = "Dummy";
+
+ [SetUp]
+ public void ClearRenderer()
+ {
+ _renderer = new RendererHTML();
+ }
+
+ [Test]
+ public void RenderBold_ThrowsException_ReceivingNullAsToken()
+ {
+ Assert.Throws(() => _renderer.RenderBold(null!));
+ }
+
+ [Test]
+ public void RenderBold_RendersCorrectly()
+ {
+ _renderer.RenderBold(
+ new BoldToken(
+ new TextToken(_dummyText)));
+ _renderer.ToString().Should().Be($"{_dummyText}");
+ }
+
+ [Test]
+ public void RenderHeader_ThrowsException_ReceivingNullAsToken()
+ {
+ Assert.Throws(() => _renderer.RenderHeader(null!));
+ }
+
+ [Test]
+ public void RenderHeader_RendersCorrectly()
+ {
+ _renderer.RenderHeader(
+ new HeaderToken(
+ new TextToken(_dummyText)));
+ _renderer.ToString().Should().Be($"{_dummyText}
");
+ }
+
+ [Test]
+ public void RenderItalic_ThrowsException_ReceivingNullAsToken()
+ {
+ Assert.Throws(() => _renderer.RenderItalic(null!));
+ }
+
+ [Test]
+ public void RenderItalic_RendersCorrectly()
+ {
+ _renderer.RenderItalic(
+ new ItalicToken(
+ new TextToken(_dummyText)));
+ _renderer.ToString().Should().Be($"{_dummyText}");
+ }
+
+ [Test]
+ public void RenderSet_ThrowsException_ReceivingNullAsToken()
+ {
+ Assert.Throws(() => _renderer.RenderSet(null!));
+ }
+
+ [Test]
+ public void RenderSet_RendersCorrectly()
+ {
+ _renderer.RenderSet(
+ new SetToken(
+ new ItalicToken(
+ new TextToken("Set")),
+ new TextToken(" "),
+ new BoldToken(
+ new TextToken("of")),
+ new TextToken(" "),
+ new TextToken("tokens")));
+ _renderer.ToString().Should().Be($"Set of tokens");
+ }
+
+ [Test]
+ public void RenderText_ThrowsException_ReceivingNullAsToken()
+ {
+ Assert.Throws(() => _renderer.RenderText(null!));
+ }
+
+ [Test]
+ public void RenderText_RendersCorrectly()
+ {
+ _renderer.RenderText(
+ new TextToken(_dummyText));
+ _renderer.ToString().Should().Be(_dummyText);
+ }
+
+ [Test]
+ public void Render_ComplexTextCorrectly()
+ {
+ _renderer.RenderHeader(
+ new HeaderToken(
+ new SetToken(
+ new ItalicToken(
+ new TextToken("Set")),
+ new TextToken(" "),
+ new BoldToken(
+ new TextToken("of")),
+ new TextToken(" "),
+ new TextToken("tokens"))));
+ _renderer.ToString().Should().Be("Set of tokens
");
+ }
+ }
+}
diff --git a/cs/Markdown.Tests/TokensTests/TokensTests.cs b/cs/Markdown.Tests/TokensTests/TokensTests.cs
new file mode 100644
index 000000000..6d0242772
--- /dev/null
+++ b/cs/Markdown.Tests/TokensTests/TokensTests.cs
@@ -0,0 +1,76 @@
+using Markdown.Tokens.HtmlTokens;
+
+namespace Markdown.Tests.TokensTests
+{
+ [TestFixture]
+ public class TokensTests
+ {
+ private readonly string _dummyText = "Hello, World!";
+ private readonly TextToken _dummyTextToken = new TextToken("Hello, World!");
+
+ [Test]
+ public void BoldToken_ThrowsException_ReceivingNullAsIRenderable()
+ {
+ Assert.Throws(() => new BoldToken(null!));
+ }
+
+ [Test]
+ public void BoldToken_ThrowsException_ReceivingNullAsIRenderer_OnRender()
+ {
+ Assert.Throws(()
+ => new BoldToken(_dummyTextToken).Render(null!));
+ }
+
+ [Test]
+ public void HeaderToken_ThrowsException_ReceivingNullAsIRenderable()
+ {
+ Assert.Throws(() => new HeaderToken(null!));
+ }
+
+ [Test]
+ public void HeaderToken_ThrowsException_ReceivingNullAsIRenderer_OnRender()
+ {
+ Assert.Throws(()
+ => new HeaderToken(_dummyTextToken).Render(null!));
+ }
+
+ [Test]
+ public void ItalicToken_ThrowsException_ReceivingNullAsIRenderable()
+ {
+ Assert.Throws(() => new ItalicToken(null!));
+ }
+
+ [Test]
+ public void ItalicToken_ThrowsException_ReceivingNullAsIRenderer_OnRender()
+ {
+ Assert.Throws(()
+ => new ItalicToken(_dummyTextToken).Render(null!));
+ }
+
+ [Test]
+ public void SetToken_ThrowsException_ReceivingNullAsIRenderable()
+ {
+ Assert.Throws(() => new SetToken(null!));
+ }
+
+ [Test]
+ public void SetToken_ThrowsException_ReceivingNullAsIRenderer_OnRender()
+ {
+ Assert.Throws(()
+ => new SetToken(_dummyTextToken).Render(null!));
+ }
+
+ [Test]
+ public void TextToken_ThrowsException_ReceivingNullAsText()
+ {
+ Assert.Throws(() => new TextToken(null!));
+ }
+
+ [Test]
+ public void TextToken_ThrowsException_ReceivingNullAsIRenderer_OnRender()
+ {
+ Assert.Throws(()
+ => new TextToken(_dummyText).Render(null!));
+ }
+ }
+}
diff --git a/cs/Markdown/Extensions/StringExtensions/DeterminingStringStartExtensions.cs b/cs/Markdown/Extensions/StringExtensions/DeterminingStringStartExtensions.cs
new file mode 100644
index 000000000..b5ae6bce7
--- /dev/null
+++ b/cs/Markdown/Extensions/StringExtensions/DeterminingStringStartExtensions.cs
@@ -0,0 +1,27 @@
+namespace Markdown.Extensions.StringExtensions
+{
+ internal static class DeterminingStringStartExtensions
+ {
+ internal static bool IsEscapeStart(this string text, int index, HashSet markers)
+ => text[index] == '\\'
+ && index + 1 < text.Length
+ && (markers.Contains(text[index + 1])
+ || text[index + 1] == '\\'
+ || index - 1 >= 0
+ && index + 1 < text.Length
+ && text[index - 1] == '#'
+ && text[index + 1] == ' ');
+
+ internal static bool IsHeaderStart(this string text, int index, string headerMarkerStart)
+ => text[index] == headerMarkerStart[0]
+ && index + 1 < text.Length
+ && text[index + 1] == headerMarkerStart[1] && (index == 0 || text[index - 1] == '\n');
+
+ internal static bool IsBoldStart(this string text, int index, string italicMarkerStart)
+ => text.IsItalicStart(index, italicMarkerStart)
+ && index + 1 < text.Length && text.IsItalicStart(index + 1, italicMarkerStart);
+
+ internal static bool IsItalicStart(this string text, int index, string italicMarkerStart)
+ => text[index] == italicMarkerStart[0];
+ }
+}
diff --git a/cs/Markdown/Extensions/StringExtensions/StringExtensions.cs b/cs/Markdown/Extensions/StringExtensions/StringExtensions.cs
new file mode 100644
index 000000000..3c7330fe1
--- /dev/null
+++ b/cs/Markdown/Extensions/StringExtensions/StringExtensions.cs
@@ -0,0 +1,73 @@
+namespace Markdown.Extensions.HandlerExtensions
+{
+ internal static class StringExtensions
+ {
+ internal static int FindEndOfLine(this string text, int startIndex)
+ {
+ var result = text.IndexOf('\n', startIndex);
+ if (result == -1)
+ {
+ return text.Length;
+ }
+ return result;
+ }
+
+ internal static bool IsSurroundedByDigits(this string text, int index, int markerLength)
+ {
+ var isLeftDigit = index - 1 >= 0 && char.IsDigit(text[index - 1]);
+ var isRightDigit = index + markerLength < text.Length
+ && char.IsDigit(text[index + markerLength]);
+ return isLeftDigit || isRightDigit;
+ }
+
+ internal static int CountConsecutiveCharacters(this string text, int index, char character)
+ {
+ var result = 0;
+ while (index + result < text.Length && text[index + result] == character)
+ {
+ result += 1;
+ }
+ return result;
+ }
+
+ internal static bool IsInsideOfWord(this string text, int index, int markerLength)
+ => index - markerLength >= 0
+ && char.IsLetter(text[index - markerLength])
+ && index + markerLength < text.Length
+ && char.IsLetter(text[index + markerLength]);
+
+ internal static int FindClosingMarker(this string text, int startIndex, string marker)
+ {
+ for (var i = startIndex; i < text.Length; i++)
+ {
+ if (char.IsWhiteSpace(text[i]))
+ {
+ var nextIndex = text.FindClosingMarker(i + 1, marker);
+ if (text.IsInsideOfWord(nextIndex, marker.Length))
+ {
+ return -1;
+ };
+ }
+
+ if (i + marker.Length > text.Length)
+ {
+ return -1;
+ }
+
+ var consecutiveCharactersCount = text.CountConsecutiveCharacters(i, marker[0]);
+ var sub = text.Substring(i, marker.Length);
+ if (sub == marker && consecutiveCharactersCount == marker.Length)
+ {
+ var preMarkerIndex = i - 1;
+ if (preMarkerIndex >= startIndex && !char.IsWhiteSpace(text[preMarkerIndex]))
+ {
+ return i;
+ }
+ }
+ i += consecutiveCharactersCount;
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj
new file mode 100644
index 000000000..f992d3aa8
--- /dev/null
+++ b/cs/Markdown/Markdown.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs
new file mode 100644
index 000000000..63e74fbd4
--- /dev/null
+++ b/cs/Markdown/Md.cs
@@ -0,0 +1,26 @@
+using Markdown.Parsers.MdParsers;
+using Markdown.Renderers;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Markdown.Tests")]
+namespace Markdown
+{
+ internal class Md(ParserMd parser, IRenderer renderer)
+ {
+ public readonly ParserMd Parser = parser ?? throw new ArgumentNullException(
+ "Передаваемый ParserMd не может быть null.");
+
+ public readonly IRenderer Renderer = renderer ?? throw new ArgumentNullException(
+ "Передаваемый IRenderer не может быть null.");
+
+ public string Render(string text)
+ {
+ var mdTokens = Parser.Parse(text);
+ foreach (var token in mdTokens)
+ {
+ token.Render(Renderer);
+ }
+ return Renderer.ToString()!;
+ }
+ }
+}
diff --git a/cs/Markdown/Parsers/IParser.cs b/cs/Markdown/Parsers/IParser.cs
new file mode 100644
index 000000000..951e199ba
--- /dev/null
+++ b/cs/Markdown/Parsers/IParser.cs
@@ -0,0 +1,9 @@
+using Markdown.Tokens.HtmlTokens;
+
+namespace Markdown.Parsers
+{
+ internal interface IParser
+ {
+ IList Parse(string text);
+ }
+}
diff --git a/cs/Markdown/Parsers/MdParsers/ParserMD.cs b/cs/Markdown/Parsers/MdParsers/ParserMD.cs
new file mode 100644
index 000000000..01803e9d4
--- /dev/null
+++ b/cs/Markdown/Parsers/MdParsers/ParserMD.cs
@@ -0,0 +1,169 @@
+using Markdown.Extensions.HandlerExtensions;
+using Markdown.Extensions.StringExtensions;
+using Markdown.Tokens.HtmlTokens;
+
+namespace Markdown.Parsers.MdParsers
+{
+ internal class ParserMd : IParser
+ {
+ private const string BoldMarkerStart = "__";
+ private const string ItalicMarkerStart = "_";
+ private const string HeaderMarkerStart = "# ";
+
+ private static readonly HashSet _markers = new HashSet()
+ {
+ '_',
+ '\n',
+ '#'
+ };
+
+ public IList Parse(string text)
+ {
+ return ParseTextPart(text);
+ }
+
+ private static List ParseTextPart(string text)
+ {
+ var tokens = new List();
+ var index = 0;
+ while (index < text.Length)
+ {
+ if (text.IsEscapeStart(index, _markers))
+ {
+ index = ParseEscape(text, index, tokens);
+ }
+ else if (text.IsHeaderStart(index, HeaderMarkerStart))
+ {
+ index = ParseHeader(text, index, tokens);
+ }
+ else if (text.IsBoldStart(index, ItalicMarkerStart))
+ {
+ index = ParseBold(text, index, tokens);
+ }
+ else if (text.IsItalicStart(index, ItalicMarkerStart))
+ {
+ index = ParseItalic(text, index, tokens);
+ }
+ else
+ {
+ index = ParseText(text, index, tokens);
+ }
+ }
+ return tokens;
+ }
+
+ private static int ParseEscape(string text, int index, List tokens)
+ {
+ tokens.Add(new TextToken(text[index + 1].ToString()));
+ return index + 2;
+ }
+
+ private static int ParseHeader(string text, int index, List tokens)
+ {
+ var startIndex = index + HeaderMarkerStart.Length;
+ var endIndex = text.FindEndOfLine(startIndex);
+ var innerTokens = ParseTextPart(text.Substring(startIndex, endIndex - startIndex));
+ endIndex += 1;
+ tokens.Add(new HeaderToken(WrapInnerTokens(innerTokens)));
+ return endIndex;
+ }
+
+ private static int ParseItalic(string text, int index, List tokens)
+ {
+ var startIndex = index + 1;
+
+ if (startIndex >= text.Length
+ || text.IsSurroundedByDigits(index, 1)
+ || char.IsWhiteSpace(text[startIndex]))
+ {
+ tokens.Add(new TextToken(ItalicMarkerStart));
+ return startIndex;
+ }
+
+ var endIndex = text.FindClosingMarker(startIndex,
+ ItalicMarkerStart);
+ if (endIndex == -1)
+ {
+ tokens.Add(new TextToken(ItalicMarkerStart));
+ return startIndex;
+ }
+
+ var innerTokens = ParseTextPart(text.Substring(startIndex, endIndex - startIndex));
+ CorrectBoldTokensInsideItalicToken(innerTokens);
+ tokens.Add(new ItalicToken(WrapInnerTokens(innerTokens)));
+ return endIndex + ItalicMarkerStart.Length;
+ }
+
+ private static void CorrectBoldTokensInsideItalicToken(IList innerTokens)
+ {
+ for (var i = 0; i < innerTokens.Count; i++)
+ {
+ if (innerTokens[i] is BoldToken boldToken)
+ {
+ var innerTextToken = (TextToken)boldToken.InnerItem;
+ innerTokens[i] = new TextToken(
+ $"{BoldMarkerStart}{innerTextToken.Text}{BoldMarkerStart}");
+ }
+ }
+ }
+
+ private static int ParseBold(string text, int index, List tokens)
+ {
+ var startIndex = index + BoldMarkerStart.Length;
+
+ if (startIndex >= text.Length
+ || text.IsSurroundedByDigits(index, BoldMarkerStart.Length)
+ || char.IsWhiteSpace(text[startIndex]))
+ {
+ tokens.Add(new TextToken(BoldMarkerStart));
+ return startIndex;
+ }
+
+ var endIndex = text.FindClosingMarker(startIndex, BoldMarkerStart);
+ if (endIndex == -1)
+ {
+ tokens.Add(new TextToken(BoldMarkerStart));
+ return startIndex;
+ }
+
+ var innerTokens = ParseTextPart(text.Substring(startIndex, endIndex - startIndex));
+ tokens.Add(new BoldToken(WrapInnerTokens(innerTokens)));
+ return endIndex + BoldMarkerStart.Length;
+ }
+
+ private static int ParseText(string text, int index, List tokens)
+ {
+ var startIndex = index;
+
+ while (index < text.Length && !_markers.Contains(text[index]))
+ {
+ if (text.IsEscapeStart(index, _markers))
+ {
+ if (index + 1 < text.Length && char.IsLetterOrDigit(text[index + 1]))
+ {
+ index += 1;
+ continue;
+ }
+ break;
+ }
+ index += 1;
+ }
+
+ if (index == startIndex)
+ {
+ index += 1;
+ }
+ tokens.Add(new TextToken(text.Substring(startIndex, index - startIndex)));
+ return index;
+ }
+
+ private static IRenderable WrapInnerTokens(IList innerTokens)
+ {
+ if (innerTokens.Count == 1)
+ {
+ return innerTokens[0];
+ }
+ return new SetToken(innerTokens.ToArray());
+ }
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Renderers/IRenderer.cs b/cs/Markdown/Renderers/IRenderer.cs
new file mode 100644
index 000000000..558ab878e
--- /dev/null
+++ b/cs/Markdown/Renderers/IRenderer.cs
@@ -0,0 +1,13 @@
+using Markdown.Tokens.HtmlTokens;
+
+namespace Markdown.Renderers
+{
+ internal interface IRenderer
+ {
+ void RenderText(TextToken textToken);
+ void RenderItalic(ItalicToken italicToken);
+ void RenderBold(BoldToken boldToken);
+ void RenderHeader(HeaderToken headerToken);
+ void RenderSet(SetToken setToken);
+ }
+}
diff --git a/cs/Markdown/Renderers/RendererHtml.cs b/cs/Markdown/Renderers/RendererHtml.cs
new file mode 100644
index 000000000..0a8cb3f40
--- /dev/null
+++ b/cs/Markdown/Renderers/RendererHtml.cs
@@ -0,0 +1,56 @@
+using Markdown.Tokens.HtmlTokens;
+using System.Text;
+
+namespace Markdown.Renderers
+{
+ internal class RendererHTML : IRenderer
+ {
+ private readonly StringBuilder _stringBuilder = new StringBuilder();
+
+ public void RenderBold(BoldToken boldToken)
+ {
+ ArgumentNullException.ThrowIfNull(boldToken);
+
+ _stringBuilder.Append("");
+ boldToken.InnerItem.Render(this);
+ _stringBuilder.Append("");
+ }
+
+ public void RenderHeader(HeaderToken headerToken)
+ {
+ ArgumentNullException.ThrowIfNull(headerToken);
+
+ _stringBuilder.Append("");
+ headerToken.InnerItem.Render(this);
+ _stringBuilder.Append("
");
+ }
+
+ public void RenderItalic(ItalicToken italicToken)
+ {
+ ArgumentNullException.ThrowIfNull(italicToken);
+
+ _stringBuilder.Append("");
+ italicToken.InnerItem.Render(this);
+ _stringBuilder.Append("");
+ }
+
+ public void RenderSet(SetToken setToken)
+ {
+ ArgumentNullException.ThrowIfNull(setToken);
+
+ foreach (var item in setToken.InnerItems)
+ {
+ item.Render(this);
+ }
+ }
+
+ public void RenderText(TextToken text)
+ {
+ ArgumentNullException.ThrowIfNull(text);
+
+ _stringBuilder.Append(text.Text);
+ }
+
+ public override string ToString() => _stringBuilder.ToString();
+ }
+}
diff --git a/cs/Markdown/Tokens/HtmlTokens/BasicMarkdownToken.cs b/cs/Markdown/Tokens/HtmlTokens/BasicMarkdownToken.cs
new file mode 100644
index 000000000..7b7fc68dd
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/BasicMarkdownToken.cs
@@ -0,0 +1,9 @@
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal abstract class BasicMarkdownToken(IRenderable innerItem)
+ {
+ public IRenderable InnerItem { get; init; }
+ = innerItem ?? throw new ArgumentNullException(nameof(innerItem),
+ "Значение не может быть null.");
+ }
+}
diff --git a/cs/Markdown/Tokens/HtmlTokens/BoldToken.cs b/cs/Markdown/Tokens/HtmlTokens/BoldToken.cs
new file mode 100644
index 000000000..bb8127e98
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/BoldToken.cs
@@ -0,0 +1,15 @@
+using Markdown.Renderers;
+
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal class BoldToken(IRenderable innerItem) : BasicMarkdownToken(innerItem), IRenderable
+ {
+ public readonly TokenTypes Type = TokenTypes.Bold;
+ public void Render(IRenderer renderer)
+ {
+ ArgumentNullException.ThrowIfNull(renderer);
+
+ renderer.RenderBold(this);
+ }
+ }
+}
diff --git a/cs/Markdown/Tokens/HtmlTokens/HeaderToken.cs b/cs/Markdown/Tokens/HtmlTokens/HeaderToken.cs
new file mode 100644
index 000000000..ef6a66cd0
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/HeaderToken.cs
@@ -0,0 +1,15 @@
+using Markdown.Renderers;
+
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal class HeaderToken(IRenderable innerItem) : BasicMarkdownToken(innerItem), IRenderable
+ {
+ public readonly TokenTypes Type = TokenTypes.Header;
+ public void Render(IRenderer renderer)
+ {
+ ArgumentNullException.ThrowIfNull(renderer);
+
+ renderer.RenderHeader(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tokens/HtmlTokens/IRenderable.cs b/cs/Markdown/Tokens/HtmlTokens/IRenderable.cs
new file mode 100644
index 000000000..555cf422d
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/IRenderable.cs
@@ -0,0 +1,9 @@
+using Markdown.Renderers;
+
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal interface IRenderable
+ {
+ void Render(IRenderer renderer);
+ }
+}
diff --git a/cs/Markdown/Tokens/HtmlTokens/ItalicToken.cs b/cs/Markdown/Tokens/HtmlTokens/ItalicToken.cs
new file mode 100644
index 000000000..f6f3d2b87
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/ItalicToken.cs
@@ -0,0 +1,15 @@
+using Markdown.Renderers;
+
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal class ItalicToken(IRenderable innerItem) : BasicMarkdownToken(innerItem), IRenderable
+ {
+ public readonly TokenTypes Type = TokenTypes.Italic;
+ public void Render(IRenderer renderer)
+ {
+ ArgumentNullException.ThrowIfNull(renderer);
+
+ renderer.RenderItalic(this);
+ }
+ }
+}
diff --git a/cs/Markdown/Tokens/HtmlTokens/SetToken.cs b/cs/Markdown/Tokens/HtmlTokens/SetToken.cs
new file mode 100644
index 000000000..5f058ef15
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/SetToken.cs
@@ -0,0 +1,20 @@
+using Markdown.Renderers;
+
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal class SetToken(params IRenderable[] innerItems) : IRenderable
+ {
+ private readonly IRenderable[] _innerItems = innerItems
+ ?? throw new ArgumentNullException(nameof(innerItems),
+ "Список внутренних токенов не может быть null.");
+ public readonly TokenTypes Type = TokenTypes.Set;
+ public IRenderable[] InnerItems => _innerItems;
+
+ public void Render(IRenderer renderer)
+ {
+ ArgumentNullException.ThrowIfNull(renderer);
+
+ renderer.RenderSet(this);
+ }
+ }
+}
diff --git a/cs/Markdown/Tokens/HtmlTokens/TextToken.cs b/cs/Markdown/Tokens/HtmlTokens/TextToken.cs
new file mode 100644
index 000000000..70a17c89a
--- /dev/null
+++ b/cs/Markdown/Tokens/HtmlTokens/TextToken.cs
@@ -0,0 +1,19 @@
+using Markdown.Renderers;
+
+namespace Markdown.Tokens.HtmlTokens
+{
+ internal class TextToken(string text) : IRenderable
+ {
+ public readonly string Text = text
+ ?? throw new ArgumentNullException(nameof(text),
+ "Список внутренних токенов не может быть null.");
+ public readonly TokenTypes Type = TokenTypes.Text;
+
+ public void Render(IRenderer renderer)
+ {
+ ArgumentNullException.ThrowIfNull(renderer);
+
+ renderer.RenderText(this);
+ }
+ }
+}
diff --git a/cs/Markdown/Tokens/TokenTypes.cs b/cs/Markdown/Tokens/TokenTypes.cs
new file mode 100644
index 000000000..521caec05
--- /dev/null
+++ b/cs/Markdown/Tokens/TokenTypes.cs
@@ -0,0 +1,11 @@
+namespace Markdown.Tokens
+{
+ enum TokenTypes
+ {
+ Bold,
+ Header,
+ Italic,
+ Set,
+ Text
+ }
+}
diff --git a/cs/clean-code.sln b/cs/clean-code.sln
index 2206d54db..58c1637e4 100644
--- a/cs/clean-code.sln
+++ b/cs/clean-code.sln
@@ -1,13 +1,15 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chess", "Chess\Chess.csproj", "{DBFBE40E-EE0C-48F4-8763-EBD11C960081}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chess", "Chess\Chess.csproj", "{DBFBE40E-EE0C-48F4-8763-EBD11C960081}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigit\ControlDigit.csproj", "{B06A4B35-9D61-4A63-9167-0673F20CA989}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlDigit", "ControlDigit\ControlDigit.csproj", "{B06A4B35-9D61-4A63-9167-0673F20CA989}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown", "Markdown\Markdown.csproj", "{FAB1F67E-C77E-4A5A-9214-A4B7C8D86B61}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown.Tests", "Markdown.Tests\Markdown.Tests.csproj", "{6767A9C8-9BBE-49B0-9B6B-9D0A271D7B4D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -23,9 +25,19 @@ Global
{B06A4B35-9D61-4A63-9167-0673F20CA989}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B06A4B35-9D61-4A63-9167-0673F20CA989}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B06A4B35-9D61-4A63-9167-0673F20CA989}.Release|Any CPU.Build.0 = Release|Any CPU
- {C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {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
+ {FAB1F67E-C77E-4A5A-9214-A4B7C8D86B61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FAB1F67E-C77E-4A5A-9214-A4B7C8D86B61}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FAB1F67E-C77E-4A5A-9214-A4B7C8D86B61}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FAB1F67E-C77E-4A5A-9214-A4B7C8D86B61}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6767A9C8-9BBE-49B0-9B6B-9D0A271D7B4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6767A9C8-9BBE-49B0-9B6B-9D0A271D7B4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6767A9C8-9BBE-49B0-9B6B-9D0A271D7B4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6767A9C8-9BBE-49B0-9B6B-9D0A271D7B4D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {94673EF3-52C0-4F5C-9907-268A715D8A8D}
EndGlobalSection
EndGlobal