Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1f02013
Предпроверка markdown
RomanGleyzer Nov 3, 2025
ffa7354
Подправил текущую реализацию
RomanGleyzer Nov 7, 2025
9b17faa
Реализация сегментера и замена интерфейса IBlock на класс Block
RomanGleyzer Nov 7, 2025
39c6c27
Первичная реализация парсера (нужно будет протестировать) с заменой и…
RomanGleyzer Nov 7, 2025
bd7fc44
Тесты для парсера
RomanGleyzer Nov 8, 2025
6588773
Правки тестов
RomanGleyzer Nov 8, 2025
51148eb
Поправил проверку на оставшиеся открытые выделения
RomanGleyzer Nov 8, 2025
94823a7
Правки тестов и парсера
RomanGleyzer Nov 8, 2025
d63c22a
Поправил баг c неправильным сбросом буфера при закрытии _ внутри откр…
RomanGleyzer Nov 9, 2025
e36bf9a
Исправил баг с закрытием em, убрал ненужные и добавил новые тесты для…
RomanGleyzer Nov 9, 2025
1ae1901
Добавил html обработчик и переписал тесты
RomanGleyzer Nov 10, 2025
7b48e1d
Доработал парсер, сделал упор на улучшение читаемости с соблюдением SRP
RomanGleyzer Nov 11, 2025
1b44b9f
Доработал парсер, добавил тест на производительность и добавил новые …
RomanGleyzer Nov 12, 2025
02adb0b
Рефакторинг Md для использования di и упрощения BlockSegmenter
RomanGleyzer Nov 15, 2025
22c4b3a
Рефакторинг HtmlRenderer. Добавление методов и констант
RomanGleyzer Nov 15, 2025
d32c6e4
Inlines в Block теперь без сеттера. Рефакторинг Md, CommitText, убрал…
RomanGleyzer Nov 15, 2025
1644149
Рефакторинг парсера и обработки экранирования
RomanGleyzer Nov 16, 2025
63d8c03
Удалил .sln файлы из репозитория
RomanGleyzer Nov 16, 2025
d19907c
Улучшил тест производительности и добавил тест по вложенности
RomanGleyzer Nov 16, 2025
f1fe8e8
Поправил тесты, добавил новые сценарии, упростил названия
RomanGleyzer Nov 17, 2025
cc310eb
add .sln
RomanGleyzer Nov 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cs/Markdown/BlockSegmenter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Markdown.Entities;
using static Markdown.Inlines.InlineSyntax;

namespace Markdown;

public class BlockSegmenter
{
private const string CarriageReturnParagraphSeparator = "\r\n\r\n";
private const string ParagraphSeparator = "\n\n";

public IReadOnlyList<Block> Segment(string text)
{
var paragraphs = SplitToParagraphs(text);
var blocks = new List<Block>();

foreach (var paragraph in paragraphs)
{
var blockType = paragraph.StartsWith(Sharp) ? BlockType.Heading : BlockType.Paragraph;
var rawText = paragraph.TrimStart('#', ' ');
var block = new Block(rawText, blockType);
blocks.Add(block);
}

return blocks;
}

private static string[] SplitToParagraphs(string text)
{
return text.Split([CarriageReturnParagraphSeparator, ParagraphSeparator], StringSplitOptions.RemoveEmptyEntries);
}
}
16 changes: 16 additions & 0 deletions cs/Markdown/Entities/Block.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Markdown.Entities;

public class Block(string rawText, BlockType type)
{
public string RawText { get; } = rawText;
Copy link

@masssha1308 masssha1308 Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set можно заменить на init в Inlines


public BlockType Type { get; } = type;

public IReadOnlyList<Node> Inlines { get; init; } = [];
}

public enum BlockType
{
Heading,
Paragraph
}
15 changes: 15 additions & 0 deletions cs/Markdown/Entities/Node.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Markdown.Entities;

public class Node(string? text, NodeType type)
{
public string? Text { get; } = text;

public NodeType Type { get; } = type;
}

public enum NodeType
{
Text,
Strong,
Em
}
122 changes: 122 additions & 0 deletions cs/Markdown/HtmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
namespace Markdown;

using Markdown.Entities;
using System.Text;

public class HtmlRenderer
{
private const string EmOpen = "<em>";
private const string EmClose = "</em>";
private const string StrongOpen = "<strong>";
private const string StrongClose = "</strong>";
private const string HeadingOpen = "<h1>";
private const string HeadingClose = "</h1>";
private const string ParagraphOpen = "<p>";
private const string ParagraphClose = "</p>";

public string Render(IReadOnlyList<Block> blocks)
{
var sb = new StringBuilder();

for (int i = 0; i < blocks.Count; i++)
{
var block = blocks[i];
var inner = RenderInlines(block.Inlines, block.Type);

switch (block.Type)
{
case BlockType.Heading:
sb.Append(HeadingOpen).Append(inner).Append(HeadingClose);
break;

case BlockType.Paragraph:
sb.Append(ParagraphOpen).Append(inner).Append(ParagraphClose);
break;
}

if (i < blocks.Count - 1)
sb.AppendLine();
}

return sb.ToString();
}

private static string RenderInlines(IReadOnlyList<Node> inlines, BlockType blockType)
{
var sb = new StringBuilder();

for (int i = 0; i < inlines.Count; i++)
{
var node = inlines[i];

switch (node.Type)
{
case NodeType.Text:
if (!string.IsNullOrEmpty(node.Text))
sb.Append(node.Text);
break;

case NodeType.Em:
RenderEm(sb, node);
break;

case NodeType.Strong:
if (blockType == BlockType.Heading)
RenderHeadingStrong(sb, inlines, ref i);
else
RenderStrong(sb, node);
break;
}
}

return sb.ToString();
}

private static void RenderEm(StringBuilder sb, Node node)
{
if (!string.IsNullOrEmpty(node.Text))
sb.Append(EmOpen).Append(node.Text).Append(EmClose);
}

private static void RenderStrong(StringBuilder sb, Node node)
{
if (!string.IsNullOrEmpty(node.Text))
sb.Append(StrongOpen).Append(node.Text).Append(StrongClose);
}

private static void RenderHeadingStrong(StringBuilder sb, IReadOnlyList<Node> inlines, ref int currentIndex)
{
sb.Append(StrongOpen);

var currentNode = inlines[currentIndex];
if (!string.IsNullOrEmpty(currentNode.Text))
sb.Append(currentNode.Text);

var nextIndex = currentIndex + 1;
while (nextIndex < inlines.Count)
{
var nextNode = inlines[nextIndex];

if (nextNode.Type == NodeType.Em)
{
if (!string.IsNullOrEmpty(nextNode.Text))
sb.Append(EmOpen).Append(nextNode.Text).Append(EmClose);

nextIndex++;
continue;
}

if (nextNode.Type == NodeType.Strong)
{
if (!string.IsNullOrEmpty(nextNode.Text))
sb.Append(nextNode.Text);

nextIndex++;
continue;
}
}

sb.Append(StrongClose);
currentIndex = nextIndex - 1;
}
}
91 changes: 91 additions & 0 deletions cs/Markdown/Inlines/InlineEscapesNormalizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Markdown.Entities;
using System.Text;
using static Markdown.Inlines.InlineSyntax;

namespace Markdown.Inlines;

public static class InlineEscapesNormalizer
{
public static string Normalize(string text)
{
if (string.IsNullOrEmpty(text))
return text;

var result = new StringBuilder(text.Length);

for (var index = 0; index < text.Length;)
{
var current = text[index];

if (current == Escape)
{
index = HandleEscape(text, index, result);
continue;
}

result.Append(current);
index++;
}

return result.ToString();
}

private static int HandleEscape(string text, int index, StringBuilder result)
{
if (index + 1 >= text.Length)
{
result.Append(Escape);
return index + 1;
}

var nextSymbol = text[index + 1];

if (nextSymbol == Underscore)
{
result.Append(PlaceholderUnderscore);
return index + 2;
}

if (nextSymbol == Sharp)
{
result.Append(PlaceholderHash);
return index + 2;
}

if (nextSymbol == Escape)
return HandleEscapedEscape(text, index, result);

result.Append(Escape).Append(nextSymbol);
return index + 2;
}

private static int HandleEscapedEscape(string text, int index, StringBuilder result)
{
var afterSymbol = index + 2 < text.Length ? text[index + 2] : EndOfText;

if (afterSymbol == Underscore || afterSymbol == Sharp)
result.Append(PlaceholderBackslash);
else
result.Append(Escape).Append(Escape);

return index + 2;
}

public static void RestorePlaceholders(List<Node> nodes)
{
for (var i = 0; i < nodes.Count; i++)
{
var text = nodes[i].Text;
if (text is null)
continue;

var restored = text
.Replace(PlaceholderUnderscore, Underscore)
.Replace(PlaceholderBackslash, Escape)
.Replace(PlaceholderHash, Sharp);

if (!ReferenceEquals(restored, text))
nodes[i] = new Node(restored, nodes[i].Type);
}
}
}
40 changes: 40 additions & 0 deletions cs/Markdown/Inlines/InlineExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Markdown.Entities;
using System.Text;

namespace Markdown.Inlines;

public static class InlineExtensions
{
public static void CommitText(this StringBuilder builder, List<Node> nodes)
{
if (builder.Length == 0) return;

nodes.Add(new Node(builder.ToString(), NodeType.Text));
builder.Clear();
}

public static void MergeTextNodes(this List<Node> nodes)
{
for (int i = 0; i < nodes.Count - 1;)
{
if (nodes[i].Type == NodeType.Text && nodes[i + 1].Type == NodeType.Text)
{
nodes[i] = new Node(nodes[i].Text + nodes[i + 1].Text, NodeType.Text);
nodes.RemoveAt(i + 1);
}
else
{
i++;
}
}
}

public static void InsertFromEnd(this StringBuilder target, List<(int index, string text)> inserts)
{
if (inserts.Count == 0) return;

inserts.Sort((a, b) => b.index.CompareTo(a.index));
foreach (var (index, text) in inserts)
target.Insert(index, text);
}
}
Loading