Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion MarkdownSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,11 @@ __Непарные_ символы в рамках одного абзаца н

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

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

# Image

Конструкция, начинающаяся с ![](), преобразуется в тег \<img src ="" alt ="">.
Внутри квадратных скобок [] указывается альтернативный текст (alt), а внутри круглых скобок () — ссылка на изображение (src).
Альтернативный текст и ссылка могут содержать остальные элементы разметки согласно общим правилам. В альтернативном тексте допускается использование символа [ только при наличии соответствующей закрывающей ].
Аналогично, в URL допускается использование символа ( только при наличии соответствующей закрывающей ).
17 changes: 17 additions & 0 deletions cs/Markdown/Markdown.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="ApprovalTests" Version="7.0.0"/>
<PackageReference Include="FluentAssertions" Version="8.8.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageReference Include="NUnit" Version="4.4.0"/>
</ItemGroup>

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

namespace Markdown;

public class Md
{
public string Render(string markdownText)
{
var tokenizer = new MarkdownTokenizer(markdownText);
var tokens = tokenizer.Tokenize();
var parser = new MarkdownParser(tokens);
var markdownDocument = parser.ParseTokens();
var htmlText = markdownDocument.ToHtml();

return htmlText;
}
}
18 changes: 18 additions & 0 deletions cs/Markdown/Nodes/Interfaces/InternalMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Markdown.Nodes.Interfaces;

public abstract class InternalMarkdownNode : MarkdownNode
{
protected InternalMarkdownNode(string value) : base(value)
{
}

public override void AddChild(MarkdownNode node)
{
Children.Add(node);
}

public override void AddChildren(List<MarkdownNode> nodes)
{
Children.AddRange(nodes);
}
}
Comment on lines 1 to 18
Copy link

@Yrwlcm Yrwlcm Nov 5, 2025

Choose a reason for hiding this comment

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

Этот класс сейчас тоже кажется избыточным. Вот мои доводы почему:

Мы хотим, чтобы наш класс MarkdownNode и все его реализации умели делать следующее:


Отдельно выделю, т.к. коммент не про это

  1. Хранить свое значение
  2. Знать своего родителя

  1. Добавлять в себя элемент
  2. Хранить весь список элементов

Будто бы сразу же приходит в голову список - ровно то, для чего он нужен. И тогда кажется логичным пускай сразу в MarkdownNode и реализуется этот список, для дерева это тоже подходит. А тут мы получается вынесли реализацию списка в отдельный класс и это прямо слишком абстракция

Сходу в голову не приходит адекватный сценарий, в котором мы решим не использовать список, а например словарь, потому что нам не нужны функции словаря, нам даже уметь доставать по индексу не нужно. А ради таких призрачных сценариев не стоит усложнять код, а просто следовать принципу KISS :)

Copy link
Author

Choose a reason for hiding this comment

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

Ответил ниже

Copy link

Choose a reason for hiding this comment

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

Нуу, в целом теперь окей, наверное если мы оставим реализацию того, что у нас листья наследуются от MarkdownNode то тогда и этот класс можно оставить с реализацией списка

8 changes: 8 additions & 0 deletions cs/Markdown/Nodes/Interfaces/LeafMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown.Nodes.Interfaces;

public abstract class LeafMarkdownNode : MarkdownNode
{
protected LeafMarkdownNode(string value) : base(value)
{
}
}
Comment on lines 1 to 8
Copy link

Choose a reason for hiding this comment

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

Тут кажется чтобы был правильный смысл, нужно поменять наследование. Мы ведь сейчас можем в листы дерева добавлять еще ноды, т.к. мне класс MarkdownNode это позволяет и я могу при желании к нему с апкастить. Пусть тогда у нас все наследуется от листа, где только значение, родитель и tohtml, а потом не листовые ноды с их списками

Ты еще занес в крайние ноды ImageNode, но что если я захочу написать например вот так

[my __bold__ image](src="http something ...")

Copy link
Author

Choose a reason for hiding this comment

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

Мы не сможем добавлять в листы дерева (или если сделаем апкаст) еще ноды, т.к там будет эксепшн. В паттерне composite у листьев оставляют методы пустыми или выбрасывают эксепшн. Еще как вариант, сделать флаг, является ли нода листом.

Насчет поменять наследование, в теории должно сработать. И как ты говорил выше, можно отказаться от такого подхода вообще, оставив только список, и в данной задаче, как я понимаю, можно не усложнять?

Copy link

@Yrwlcm Yrwlcm Nov 8, 2025

Choose a reason for hiding this comment

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

Вот эта часть мне не очень нравится в этом решении, хоть это и по паттерну

там будет эксепшн

Точнее, я не понимал в каком контексте мы будем этот метод .Add вызывать и каким способом мы будем понимать можем мы это сделать или нет. Если бы нам пришлось заворачивать что-то в трай кетч, это было бы плохо. Т.к. конкретно это исключение - часть ожидаемой логики, что у нас будут листья дерева. А не что-то непредвиденное

С учетом того, что переделали решение на токенайзер и парсер и теперь парсер должен сам понимать у кого что он может вызывать, чтобы собрать дерево - теперь такой подход норм и его можно оставить)

22 changes: 22 additions & 0 deletions cs/Markdown/Nodes/Interfaces/MarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Markdown.Nodes.Interfaces;

public abstract class MarkdownNode
{
public readonly List<MarkdownNode> Children = [];
public readonly string Value;

protected MarkdownNode(string value)
{
Value = value;
}

public virtual void AddChild(MarkdownNode node)
{
}

public virtual void AddChildren(List<MarkdownNode> nodes)
{
}

public abstract string ToHtml();
}
20 changes: 20 additions & 0 deletions cs/Markdown/Nodes/Internal/AltNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text;
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Internal;

public class AltNode : InternalMarkdownNode
{
public AltNode(string value) : base(value)
{
}

public override string ToHtml()
{
var textBuilder = new StringBuilder();
foreach (var child in Children)
textBuilder.Append(child.ToHtml());

return textBuilder.ToString();
}
}
19 changes: 19 additions & 0 deletions cs/Markdown/Nodes/Internal/BoldNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text;
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Internal;

public class BoldNode : InternalMarkdownNode
{
public BoldNode(string value) : base(value)
{
}

public override string ToHtml()
{
var textBuilder = new StringBuilder();
foreach (var child in Children) textBuilder.Append(child.ToHtml());

return $"<strong>{textBuilder}</strong>";
}
}
24 changes: 24 additions & 0 deletions cs/Markdown/Nodes/Internal/HeaderNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text;
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Internal;

public class HeaderNode : InternalMarkdownNode
{
public HeaderNode(string value) : base(value)
{
}

public override string ToHtml()
{
var textBuilder = new StringBuilder();
var controlCharacters = new StringBuilder();
foreach (var child in Children)
if (child.Value is "\n" or "\r")
controlCharacters.Append(child.ToHtml());
else
textBuilder.Append(child.ToHtml());

return $"<h1>{textBuilder}</h1>{controlCharacters}";
}
}
17 changes: 17 additions & 0 deletions cs/Markdown/Nodes/Internal/ImageNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Internal;

public class ImageNode : InternalMarkdownNode
{
public ImageNode(string value) : base(value)
{
}

public override string ToHtml()
{
var alt = Children[0].ToHtml();
var url = Children[1].ToHtml();
return $"<img src =\"{url}\" alt=\"{alt}\">";
}
}
19 changes: 19 additions & 0 deletions cs/Markdown/Nodes/Internal/ItalicNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text;
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Internal;

public class ItalicNode : InternalMarkdownNode
{
public ItalicNode(string value) : base(value)
{
}

public override string ToHtml()
{
var textBuilder = new StringBuilder();
foreach (var child in Children) textBuilder.Append(child.ToHtml());

return $"<em>{textBuilder}</em>";
}
}
20 changes: 20 additions & 0 deletions cs/Markdown/Nodes/Internal/MarkdownDocumentNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text;
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Internal;

public class MarkdownDocumentNode : InternalMarkdownNode
{
public MarkdownDocumentNode(string value) : base(value)
{
}

public override string ToHtml()
{
var textBuilder = new StringBuilder();
foreach (var child in Children)
textBuilder.Append(child.ToHtml());

return textBuilder.ToString();
}
}
15 changes: 15 additions & 0 deletions cs/Markdown/Nodes/Leaf/TextNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Leaf;

public class TextNode : LeafMarkdownNode
{
public TextNode(string value) : base(value)
{
}

public override string ToHtml()
{
return Value;
}
}
15 changes: 15 additions & 0 deletions cs/Markdown/Nodes/Leaf/UrlNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Markdown.Nodes.Interfaces;

namespace Markdown.Nodes.Leaf;

public class UrlNode : LeafMarkdownNode
{
public UrlNode(string value) : base(value)
{
}

public override string ToHtml()
{
return Value;
}
}
28 changes: 28 additions & 0 deletions cs/Markdown/Parsers/EscapeParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Markdown.Nodes.Interfaces;
using Markdown.Nodes.Leaf;
using Markdown.Parsers.Interfaces;
using Markdown.Tokenizer;

namespace Markdown.Parsers;

public class EscapeParser : IParser
{
private readonly MarkdownParser parser;

public EscapeParser(MarkdownParser parser)
{
this.parser = parser;
}

public ParseStatus TryParse(out MarkdownNode node)
{
node = new TextNode(@"\");
if (parser.CurrentToken.Type != TokenType.Escape)
return ParseStatus.Fail();

parser.MoveNext();
node = new TextNode($"{parser.CurrentToken.Value}");

return ParseStatus.Ok();
}
}
44 changes: 44 additions & 0 deletions cs/Markdown/Parsers/HeaderParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Markdown.Nodes.Interfaces;
using Markdown.Nodes.Internal;
using Markdown.Nodes.Leaf;
using Markdown.Parsers.Interfaces;
using Markdown.Tokenizer;

namespace Markdown.Parsers;

public class HeaderParser : IParser
{
private readonly MarkdownParser parser;
private readonly HashSet<TokenType> expectedStopSymbols = [TokenType.NewLine, TokenType.Carriage];

public HeaderParser(MarkdownParser parser)
{
this.parser = parser;
}

public ParseStatus TryParse(out MarkdownNode node)
{
if (parser.NextToken != null
&& (parser.CurrentToken.Type != TokenType.Hash
|| (parser.CurrentToken.Type == TokenType.Hash && parser.NextToken.Type != TokenType.Space)))
{
node = new TextNode("#");
return ParseStatus.Fail();
}
parser.MoveNext();

node = new HeaderNode("#");
parser.ParentStack.Push(parser.CurrentParent);
parser.CurrentParent = node;
parser.MoveNext();
parser.ParseTokens(null, expectedStopSymbols);

if (parser.CurrentToken.Type != TokenType.Eof)
node.AddChild(new TextNode($"{parser.CurrentToken.Value}"));

if (!expectedStopSymbols.Contains(parser.CurrentToken.Type))
parser.CurrentParent = parser.ParentStack.Pop();

return ParseStatus.Ok();
}
}
Loading