Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d37d48c
Провел проектирование, выделил интерфейсы с классами и extension мето…
gogy4 Nov 3, 2025
0739f53
изменить метод Wrap в зависимости от TokenType, провел небольшой рефа…
gogy4 Nov 3, 2025
3fb0ae8
Добавил новые токены, реализовал логику Tokenizer.cs и TagRender.cs
gogy4 Nov 4, 2025
c4a3f90
Перенес метод RenderLine в класс Render.cs и добавил некоторую логику…
gogy4 Nov 4, 2025
87a105f
Написал тесты, который проверяют логику работы рендера с _ и __. Поло…
gogy4 Nov 4, 2025
6c74bd6
Добавил тест на производительность
gogy4 Nov 4, 2025
8f77b1f
маленький рефакторинг
gogy4 Nov 4, 2025
4bece38
Добавил логику рендера для header. Написал тест для проверки новой ло…
gogy4 Nov 4, 2025
7c4967d
Сделал доп. класс для работы с тегами через стек
gogy4 Nov 5, 2025
818e646
Сделал классы, которые служат, только как данные рекордами. Добавил п…
gogy4 Nov 5, 2025
d2ef385
Убрал лишний таб в TagRender.cs. Вынес в отдельный статический класс …
gogy4 Nov 5, 2025
3dd1379
Порефакторил if, теперь все if начинаются с новой строки и скобок. Пе…
gogy4 Nov 5, 2025
2ca6b3f
Реализовал всю логику в Render.cs которая была описана на минимальный…
gogy4 Nov 5, 2025
d2b239c
Вынес логику проверки на закрытие тега в отдельный метод
gogy4 Nov 5, 2025
db6590b
Update MarkdownTests.cs
gogy4 Nov 6, 2025
cd9e0f4
Вынес новые enum для удобной работы с токенами. Сделал Token.cs и Tag…
gogy4 Nov 7, 2025
949c254
Merge remote-tracking branch 'origin/master'
gogy4 Nov 7, 2025
794afe9
Поменял логику в связи изменений дто
gogy4 Nov 7, 2025
b2d5fad
Переименовал класс рендера, вынес в отдельную логику работу с тегами,…
gogy4 Nov 7, 2025
78ccd27
Поменял тест, который проверяет работу за O(n), теперь выполняется не…
gogy4 Nov 7, 2025
5d37e7c
Добавил обработку нового тега - ссылки
gogy4 Nov 8, 2025
eb9d6b7
порефакторил код, добавил обработку ссылки, вынес повторяющийся код в…
gogy4 Nov 8, 2025
e62c0f0
Изменил текст в тестах, добавил новый тест
gogy4 Nov 8, 2025
f3157cf
Вынес работу с контентом в TagContext.cs, который реализует IDisposab…
gogy4 Nov 10, 2025
efc74a3
Допушил класс CharContext.cs
gogy4 Nov 10, 2025
0042b1f
Сделал обработку токенов через паттерн "Стратегия". Создал абстракцию…
gogy4 Nov 11, 2025
33ec3de
Применил паттерн "Стратегия" в TagProcessor.cs. Порефакторил классы.
gogy4 Nov 11, 2025
a95fcea
Небольшой рефактор теста Markdown_RenderTime_ShouldScaleLinearly
gogy4 Nov 11, 2025
fd494f6
Изменил MarkdownSpec.md
gogy4 Nov 11, 2025
4dbd8ac
Исправил MarkdownSpec.md
gogy4 Nov 11, 2025
bae770a
Исправил экранирование в MarkdownSpec.md
gogy4 Nov 11, 2025
f032539
Еще немного фиксов с экранированием
gogy4 Nov 11, 2025
49faf43
Фикс экранирования полужирного символа
gogy4 Nov 11, 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
40 changes: 39 additions & 1 deletion MarkdownSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,42 @@ __Непарные_ символы в рамках одного абзаца н

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

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



# Ссылки

Ссылка представлена строго в виде: \[текст ссылки](URL). Выделяется тегом \<a href="url">text\</a>
Текст ссылки не должен начинаться с пробела и в скбоках с URL не должно быть пробелов, иначе это не считается выделением

Таким образом

Ссылка с текстом: \[репозиторий gogy](https://github.com/gogy4?tab=repositories)

превратится в:
\<a href=\"https://github.com/gogy4?tab=repositories\">репозиторий gogy\</a>

В тексте ссылки могут присутствовать и другие символы разметки с правилами:

Нельзя выделять весь текст ссылки в другие символы, например:
\[\_\_Полужирная ссылка\_\_](google.com) так и останется

Аналогично и с курсивом:
\[\_Курсивная ссылка\_](google.com) так и останется

Другие символы разметки можно использовать только на учатски, а не на весь текст, например ссылка:
\[Ссылка с \_курсивным\_ текстом](google.com)

первратится в:
\<a href="google.com:>Ссылка с \<em>курсивным\</em> текстом\</a>

Аналогично с полужирным текстом:
\[Ссылка с \_\_полужирным\_\_ текстом](google.com)

превратится в:
\<a href="google.com:>Ссылка с \<strong>полужирным\</strong> текстом\</a>

Чтобы экранировать текст с ссылкой, достаточно поставить знак экранирования перед [

\\\[Ссылка с текстом](google.com)
3 changes: 3 additions & 0 deletions cs/Markdown/Dto/CharContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Markdown.Dto;

public record CharContext(char? Next, bool CanOpen, bool CanClose);
46 changes: 46 additions & 0 deletions cs/Markdown/Dto/Tag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Text;

namespace Markdown.Dto
{
public class Tag
{
public Token Token { get; }
public StringBuilder Content { get; }
public bool IsOpen { get; }

public bool ContainsSpace { get; private set; }
public bool HasOnlyDigits { get; private set; } = true;

public Tag(Token token, StringBuilder content, bool isOpen)
{
Token = token;
Content = content;
IsOpen = isOpen;
UpdateFlags(content.ToString());
}

public void Append(string text)
{
Content.Append(text);
UpdateFlags(text);
}

private void UpdateFlags(string text)
{
if (!ContainsSpace && text.Contains(' '))
{
ContainsSpace = true;
}

if (!HasOnlyDigits)
{
return;
}
foreach (var ch in text.Skip(Token.Value.Length).Where(ch => !char.IsDigit(ch)))
{
HasOnlyDigits = false;
break;
}
}
}
}
54 changes: 54 additions & 0 deletions cs/Markdown/Dto/Token.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace Markdown.Dto
{
public class Token
{
public string Value { get; private set; }
public TokenType Type { get; private set; }
public TokenRole Role { get; private set; }
public TokenPosition Position { get; private set; }

public Token(string value, TokenType type, TokenRole role, TokenPosition position)
{
Value = value;
Type = type;
Role = role;
Position = position;
}

public Token(string value, TokenType type, bool canOpen, bool canClose)
{
Value = value;
Type = type;
DetermineRole(canOpen, canClose);
DeterminePosition(canOpen, canClose);
}

private void DetermineRole(bool canOpen, bool canClose)
{
Determine(canOpen, canClose, v => Role = v,
TokenRole.Both, TokenRole.Open, TokenRole.Close);
}

private void DeterminePosition(bool canOpen, bool canClose)
{
Determine(canOpen, canClose, v => Position = v,
TokenPosition.Inside, TokenPosition.Begin, TokenPosition.End);
}

private void Determine<T>(bool canOpen, bool canClose, Action<T> setter, T both, T open, T close)
{
if (canOpen == canClose)
{
setter(both);
}
else if (canOpen)
{
setter(open);
}
else
{
setter(close);
}
}
}
}
9 changes: 9 additions & 0 deletions cs/Markdown/Dto/TokenPosition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Markdown.Dto;

public enum TokenPosition
{
None = 0,
Begin,
Inside,
End,
}
9 changes: 9 additions & 0 deletions cs/Markdown/Dto/TokenRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Markdown.Dto;

public enum TokenRole
{
None = 0,
Open,
Close,
Both,
}
12 changes: 12 additions & 0 deletions cs/Markdown/Dto/TokenType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Markdown.Dto;

public enum TokenType

This comment was marked as resolved.

{
Text = 0,
Italic,
Strong,
Escape,
Header,
Link,
End
}
30 changes: 30 additions & 0 deletions cs/Markdown/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Markdown.Extensions;

public static class StringExtensions
{
public static IEnumerable<string> EnumerateLines(this string? markdown)
{
if (markdown is null)
{
yield break;
}

var start = 0;

for (var i = 0; i < markdown.Length; i++)
{
if (markdown[i] != '\n')
{
continue;
}

yield return markdown[start..i];
start = i + 1;
}

if (start <= markdown.Length)
{
yield return markdown[start..];
}
}
}
16 changes: 16 additions & 0 deletions cs/Markdown/Markdown.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions cs/Markdown/RenderServices/Abstractions/IRender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Markdown;

public interface IRender
{
public string RenderText(string markdown);
}
33 changes: 33 additions & 0 deletions cs/Markdown/RenderServices/Implementations/MarkdownRender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text;
using Markdown.Dto;
using Markdown.Extensions;
using Markdown.TagUtils.Abstractions;
using Markdown.TagUtils.Implementations;
using Markdown.TokensUtils;
using Markdown.TokensUtils.Abstractions;
using Markdown.TokensUtils.Implementations;

namespace Markdown
{
public class MarkdownRender(ITokenizer tokenizer, ITagProcessor tagProcessor) : IRender
{
public string RenderText(string markdown)
{
var markDownLines = markdown.EnumerateLines();
var result = new StringBuilder();
foreach (var line in markDownLines)
{
if (result.Length > 0) result.Append('\n');
result.Append(RenderLine(line));
}

return result.ToString();
}

private string RenderLine(string line)
{
var tokens = tokenizer.Tokenize(line).ToList();
return tagProcessor.Process(tokens);
}
}
}
53 changes: 53 additions & 0 deletions cs/Markdown/TagUtils/Abstractions/BaseTagStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Text;
using Markdown.Dto;
using Markdown.TagUtils.Abstractions;

public abstract class BaseTagStrategy(ITagContext context)
{
protected void ProcessTag(Token token, bool isStrong, Func<Tag, Token, bool> isInvalidCloseContext)
{
var tagsStack = context.Tags;
var parent = tagsStack.Count > 0 ? tagsStack.Peek() : null;
var canClose = isStrong ? token.Role is TokenRole.Close : token.Role is TokenRole.Close or TokenRole.Both;
var canOpen = isStrong
? token.Role is TokenRole.Open or TokenRole.Both ||
(token.Role == TokenRole.Close && tagsStack.Count > 0 && tagsStack.Peek().IsOpen)
: token.Role is TokenRole.Open or TokenRole.Both;

if (canClose && tagsStack.Count > 0 && !isInvalidCloseContext(parent, token))
{
if (isStrong)
{
var popParent = tagsStack.Pop();
var grandParent = tagsStack.Count > 0 ? tagsStack.Peek() : null;
var shouldNotClose = grandParent is not null && grandParent.Token.Type == TokenType.Italic &&
grandParent.IsOpen;

if (shouldNotClose)
{
var newContent = popParent.Content.Append(popParent.Token.Value);
context.Append(newContent.ToString());
}
else
{
tagsStack.Push(popParent);
context.CloseTop();
}
}
else
{
context.CloseTop();
}
}
else if (canOpen)
{
context.Open(new Tag(token, new StringBuilder(token.Value), true));
}
else
{
context.Append(token.Value);
}
}

public abstract void Process(Token token);
}
14 changes: 14 additions & 0 deletions cs/Markdown/TagUtils/Abstractions/ITagContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text;
using Markdown.Dto;

namespace Markdown.TagUtils.Abstractions;

public interface ITagContext : IDisposable
{
public void Append(string content);
public void CloseTop(bool isFinal = false);
public void Open(Tag tag);
public Stack<Tag> Tags { get; }
public StringBuilder Content { get; }
public bool SkipNextAsMarkup { get; set; }
}
9 changes: 9 additions & 0 deletions cs/Markdown/TagUtils/Abstractions/ITagProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text;
using Markdown.Dto;

namespace Markdown.TagUtils.Abstractions;

public interface ITagProcessor
{
public string Process(IEnumerable<Token> tokens);
}
Loading