Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9521946
init commit: накидал начальную структуру.
maximka200 Oct 31, 2025
792c4fc
- Сделал все классы (кроме тех, что лежат в Domains) статическими
maximka200 Nov 3, 2025
6fc0993
Добавил тестов и вынес метод CollectFullValue в MdLexer.cs
maximka200 Nov 4, 2025
aa264ce
- Создал класс HeaderNode - наследника Node
maximka200 Nov 6, 2025
9f1d158
правки замечаний
maximka200 Nov 6, 2025
cc8ea9e
- Вынес TextNode в отдельный класс
maximka200 Nov 7, 2025
33fc30c
- fix TextNode.cs
maximka200 Nov 7, 2025
1814946
- Написал тесты
maximka200 Nov 7, 2025
e379eaf
- переписал некоторые тесты
maximka200 Nov 7, 2025
db027d7
сделал корректное определение подчеркивания в словах с числами, разны…
maximka200 Nov 8, 2025
78c9a9a
- написал еще методов в ListMdTokenExtension.cs и поправил старые
maximka200 Nov 8, 2025
d44899e
косметические правки
maximka200 Nov 8, 2025
8257f70
- написал тест на HtmlGenerator.cs
maximka200 Nov 9, 2025
4ba0ed4
Декомпозировал Parse
maximka200 Nov 9, 2025
6d74d7c
- Написал сквозные тесты
maximka200 Nov 9, 2025
7e3a534
написал перформанс тест
maximka200 Nov 9, 2025
aee479e
- реализовал ссылки (еще не написал по ним спеки)
maximka200 Nov 10, 2025
0e1858f
- улучшил читаемость
maximka200 Nov 11, 2025
f126b28
написал спеки для гиперссылки
maximka200 Nov 11, 2025
68dc63a
- правки по MdLexer.cs
maximka200 Nov 13, 2025
c220d12
- правки по TokenParser.cs
maximka200 Nov 13, 2025
2a710e4
- уточнил спеки
maximka200 Nov 13, 2025
d24a8d7
переименовал неймспейс NodeTypes -> Nodes
maximka200 Nov 13, 2025
34850a4
дописал тестов на header и пофиксил методы для его парсинга
maximka200 Nov 13, 2025
1d4e962
- пофиксил основные замечания
maximka200 Nov 21, 2025
505a973
- переписал структуру генерации Html
maximka200 Nov 24, 2025
5cd805f
- переписал на Try метод
maximka200 Nov 24, 2025
937b8ab
- поправил тесты
maximka200 Nov 24, 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
22 changes: 21 additions & 1 deletion MarkdownSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,24 @@ __Непарные_ символы в рамках одного абзаца н

превратится в:

\<h1>Заголовок \<strong>с \<em>разными\</em> символами\</strong>\</h1>
\<h1>Заголовок \<strong>с \<em>разными\</em> символами\</strong>\</h1>

# Гиперссылки

Текст вида \[текст ссылки](адрес ссылки) должен превращаться в гиперссылку:

\[Пример ссылки](https://example.com) превратится в:
```
<a href="https://example.com">Пример ссылки</a>.
```

Внутри текста ссылки и адреса ссылки могут использоваться символы разметки (кроме символов отвечающих за гиперссылки), которые будут обработаны по общим правилам:
- \[Ссылка с \_выделением_](https://example.com/__путь__/to/resource) превратится в:
```
<a href="https://example.com<strong>путь</strong>/to/resource">Ссылка с <em>выделением</em></a>.
```
- \__\[Такая ссылка](https://example.com/__путь__/to/resource)\__ превратится в:

```
<strong><a href="https://example.com/путь/to/resource">Такая ссылка</em></a></strong>
```
8 changes: 8 additions & 0 deletions cs/Markdown/Domains/LinkNodeType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown.Domains;

public enum LinkNodeType
{
LinkRoot,
LinkText,
MeaningText
}
17 changes: 17 additions & 0 deletions cs/Markdown/Domains/MdToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Markdown.Domains;

public class MdToken(TokenType type, string value)
{
public TokenType Type { get; } = type;
public string Value { get; } = value;

public override bool Equals(object? obj)
{
if (obj is not MdToken other)
return false;

return Type == other.Type && Value == other.Value;
}

public override int GetHashCode() => HashCode.Combine(Type, Value);
}
22 changes: 22 additions & 0 deletions cs/Markdown/Domains/Node.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text;

namespace Markdown.Domains;

public abstract class Node
{
public List<Node> Children { get; } = [];

protected Node(List<Node>? children = null)
{
if (children != null)
Children.AddRange(children);
}

public abstract void ConvertToHtml(StringBuilder sb);

protected void RenderChildren(StringBuilder sb)
{
foreach (var child in Children)
child.ConvertToHtml(sb);
}
}
7 changes: 7 additions & 0 deletions cs/Markdown/Domains/NodeContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Markdown.Domains;

public enum NodeContext
{
Italic,
None
}
13 changes: 13 additions & 0 deletions cs/Markdown/Domains/Nodes/BoldNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class BoldNode(List<Node>? children = null) : Node(children)
{
public override void ConvertToHtml(StringBuilder sb)
{
sb.Append("<strong>");
RenderChildren(sb);
sb.Append("</strong>");
}
}
28 changes: 28 additions & 0 deletions cs/Markdown/Domains/Nodes/HeaderNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class HeaderNode : Node
{
public const int MaxHeaderLevel = 6;
private int Level { get; }

public HeaderNode(int level = 1, List<Node>? children = null)
: base(children)
{
if (level is < 1 or > MaxHeaderLevel)
throw new ArgumentOutOfRangeException(
nameof(level),
$"Header level must be between 1 and {MaxHeaderLevel}."
);

Level = level;
}

public override void ConvertToHtml(StringBuilder sb)
{
sb.Append($"<h{Level}>");
RenderChildren(sb);
sb.Append($"</h{Level}>");
}
}
13 changes: 13 additions & 0 deletions cs/Markdown/Domains/Nodes/ItalicNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class ItalicNode(List<Node>? children = null) : Node(children)
{
public override void ConvertToHtml(StringBuilder sb)
{
sb.Append("<em>");
RenderChildren(sb);
sb.Append("</em>");
}
}
43 changes: 43 additions & 0 deletions cs/Markdown/Domains/Nodes/LinkNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class LinkNode(LinkNodeType type, List<Node>? children = null) : Node(children)
{
private LinkNodeType LinkNodeType { get; } = type;

public override void ConvertToHtml(StringBuilder sb)
{
if (LinkNodeType != LinkNodeType.LinkRoot || Children.Count < 2)
{
RenderChildren(sb);
return;
}

var textNode = Children
.OfType<LinkNode>()
.FirstOrDefault(n => n.LinkNodeType == LinkNodeType.MeaningText);

var urlNode = Children
.OfType<LinkNode>()
.FirstOrDefault(n => n.LinkNodeType == LinkNodeType.LinkText);

if (textNode == null || urlNode == null)
{
RenderChildren(sb);
return;
}

var textBuilder = new StringBuilder();
textNode.ConvertToHtml(textBuilder);

var urlBuilder = new StringBuilder();
urlNode.ConvertToHtml(urlBuilder);

sb.Append("<a href=\"");
sb.Append(urlBuilder);
sb.Append("\">");
sb.Append(textBuilder);
sb.Append("</a>");
}
}
11 changes: 11 additions & 0 deletions cs/Markdown/Domains/Nodes/NewLineNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class NewLineNode : Node
{
public override void ConvertToHtml(StringBuilder sb)
{
sb.Append("<br/>");
}
}
18 changes: 18 additions & 0 deletions cs/Markdown/Domains/Nodes/RootNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class RootNode(List<Node>? children = null) : Node(children)
{
public override void ConvertToHtml(StringBuilder sb)
{
RenderChildren(sb);
}

public string ToHtml()
{
var builder = new StringBuilder();
ConvertToHtml(builder);
return builder.ToString();
}
}
13 changes: 13 additions & 0 deletions cs/Markdown/Domains/Nodes/TextNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text;

namespace Markdown.Domains.Nodes;

public class TextNode(string text) : Node
{
private string Text { get; } = text;

public override void ConvertToHtml(StringBuilder sb)
{
sb.Append(Text);
}
}
16 changes: 16 additions & 0 deletions cs/Markdown/Domains/TokenType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Markdown.Domains;

public enum TokenType
{
Word,
Number,
Space,
Underscore,
Grid,
Escape,
Slash,
LeftSquareBracket,
RightSquareBracket,
LeftParenthesis,
RightParenthesis
}
103 changes: 103 additions & 0 deletions cs/Markdown/Lexer/MdLexer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Text;
using Markdown.Domains;

namespace Markdown.Lexer;

/// <summary>
/// Разбивает входной текст на последовательность Md-токенов (<see cref="TokenType"/>).
/// </summary>
/// <remarks>
/// Поддерживаемые типы токенов:
/// <list type="bullet">
/// <item><description><see cref="TokenType.Word"/> — последовательность буквенных символов.</description></item>
/// <item><description><see cref="TokenType.Number"/> — последовательность цифр.</description></item>
/// <item><description><see cref="TokenType.Space"/> — пробельный символ пробела.</description></item>
/// <item><description>
/// <see cref="TokenType.Tab"/> — символ табуляции, который при разборе
/// заменяется на <see cref="MdLexer.SpacesCountInTab"/> пробелов.
/// </description></item>
/// <item><description><see cref="TokenType.Underscore"/> — символ подчёркивания (<c>_</c>).</description></item>
/// <item><description><see cref="TokenType.Grid"/> — символ решётки (<c>#</c>).</description></item>
/// <item><description><see cref="TokenType.Escape"/> — символ экранирования (<c>\</c>).</description></item>
/// <item><description><see cref="TokenType.Slash"/> — слэш (<c>/</c>).</description></item>
/// <item><description><see cref="TokenType.LeftSquareBracket"/> — левая квадратная скобка (<c>[</c>).</description></item>
/// <item><description><see cref="TokenType.RightSquareBracket"/> — правая квадратная скобка (<c>]</c>).</description></item>
/// <item><description><see cref="TokenType.LeftParenthesis"/> — левая круглая скобка (<c>(</c>).</description></item>
/// <item><description><see cref="TokenType.RightParenthesis"/> — правая круглая скобка (<c>)</c>).</description></item>
/// </list>
/// </remarks>
public static class MdLexer
{
private static readonly Dictionary<char, TokenType> TokenMap = new()
{
{ '#', TokenType.Grid },
{ '_', TokenType.Underscore },
{ ' ', TokenType.Space },
{ '\u00a0', TokenType.Space },
{ '\u200b', TokenType.Space },
{ '\t', TokenType.Space },
{ '\n', TokenType.Escape },
{ '\r', TokenType.Escape },
{ '\\', TokenType.Slash },
{ '[', TokenType.LeftSquareBracket },
{ ']', TokenType.RightSquareBracket },
{ '(', TokenType.LeftParenthesis },
{ ')', TokenType.RightParenthesis }
};

public static List<MdToken> Tokenize(string text)
{
var tokens = new List<MdToken>();

for (var i = 0; i < text.Length; i++)
{
var symbol = text[i];
var tokenType = GetTokenType(symbol);

switch (tokenType)
{
case TokenType.Word or TokenType.Number:
var (value, nextIndex) = CollectFullValue(text, i,
tokenType is TokenType.Word ? IsPieceOfWord : char.IsNumber
);
tokens.Add(new MdToken(tokenType, value));
i = nextIndex;
break;
default:
tokens.Add(new MdToken(tokenType, symbol.ToString()));
break;
}
}

return tokens;
}

public static TokenType GetTokenType(char text)
{
if (TokenMap.TryGetValue(text, out var tokenType))
return tokenType;

return char.IsNumber(text) ? TokenType.Number : TokenType.Word;
}

private static (string word, int nextIndex) CollectFullValue(string text, int startIndex,
Func<char, bool> predicate)
{
var value = new StringBuilder();
value.Append(text[startIndex]);

var i = startIndex + 1;
while (i < text.Length && predicate(text[i]))
{
value.Append(text[i]);
i++;
}

return (value.ToString(), i - 1);
}

private static bool IsPieceOfWord(this char ch)
{
return char.IsLetter(ch) || !TokenMap.ContainsKey(ch);
}
}
9 changes: 9 additions & 0 deletions cs/Markdown/Markdown.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
17 changes: 17 additions & 0 deletions cs/Markdown/Md.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Markdown.Lexer;
using Markdown.Parser;

namespace Markdown;

public static class Md
{
public static string Render(string text)
{
var tokens = MdLexer.Tokenize(text);
var parser = new TokenParser(tokens);
var rootNode = parser.Parse();
var html = rootNode.ToHtml();

return html;
}
}
Loading