Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Минеев Максим #243

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,4 @@ _Pvt_Extensions
**/.idea
**/.vscode
**/node_modules
/cs/Markdown/Markdown.csproj.DotSettings
9 changes: 9 additions & 0 deletions cs/Markdown/AstNodes/BoldMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.Enums;

namespace Markdown.AstNodes;

public class BoldMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren
{
public override MarkdownNodeName Type => MarkdownNodeName.Bold;
public List<MarkdownNode> Children { get; } = [];
}
9 changes: 9 additions & 0 deletions cs/Markdown/AstNodes/HeadingMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.Enums;

namespace Markdown.AstNodes;

public class HeadingMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren
{
public override MarkdownNodeName Type => MarkdownNodeName.Heading;
public List<MarkdownNode> Children { get; } = [];
}
6 changes: 6 additions & 0 deletions cs/Markdown/AstNodes/IMarkdownNodeWithChildren.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Markdown.AstNodes;

public interface IMarkdownNodeWithChildren
{
public List<MarkdownNode> Children { get; }
}
9 changes: 9 additions & 0 deletions cs/Markdown/AstNodes/ItalicMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.Enums;

namespace Markdown.AstNodes;

public class ItalicMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren
{
public override MarkdownNodeName Type => MarkdownNodeName.Italic;
public List<MarkdownNode> Children { get; } = [];
}
17 changes: 17 additions & 0 deletions cs/Markdown/AstNodes/MarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Markdown.Enums;

namespace Markdown.AstNodes;

public abstract class MarkdownNode
{
public abstract MarkdownNodeName Type { get; }

public override bool Equals(object? obj)
{
if (this is IMarkdownNodeWithChildren node && obj is IMarkdownNodeWithChildren other)
return this.GetType() == other.GetType() && node.Children.SequenceEqual(other.Children);
if (this is TextMarkdownNode valueNode && obj is TextMarkdownNode otherValueNode)
return valueNode.Content.Equals(otherValueNode.Content);
return false;
}
}
9 changes: 9 additions & 0 deletions cs/Markdown/AstNodes/RootMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.Enums;

namespace Markdown.AstNodes;

public class RootMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren
{
public override MarkdownNodeName Type => MarkdownNodeName.Root;
public List<MarkdownNode> Children { get; } = [];
}
9 changes: 9 additions & 0 deletions cs/Markdown/AstNodes/TextMarkdownNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.Enums;

namespace Markdown.AstNodes;

public class TextMarkdownNode(string content) : MarkdownNode
{
public override MarkdownNodeName Type => MarkdownNodeName.Text;
public string Content => content;
}
10 changes: 10 additions & 0 deletions cs/Markdown/Enums/MarkdownNodeName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Markdown.Enums;

public enum MarkdownNodeName
{
Bold,
Italic,
Heading,
Text,
Root,
}
12 changes: 12 additions & 0 deletions cs/Markdown/Enums/MarkdownTokenName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Markdown.Enums;

public enum MarkdownTokenName
{
Italic,
Bold,
Heading,
Text,
NewLine,
Space,
Number,
}
8 changes: 8 additions & 0 deletions cs/Markdown/Interfaces/ILexer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Markdown.Tokens;

namespace Markdown;

public interface ILexer
{
List<Token> Tokenize(string input);
}
9 changes: 9 additions & 0 deletions cs/Markdown/Interfaces/IParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Markdown.AstNodes;
using Markdown.Tokens;

namespace Markdown;

public interface IParser
{
RootMarkdownNode Parse(List<Token> tokens);
}
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>
208 changes: 208 additions & 0 deletions cs/Markdown/MarkdownLexer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System.Text;
using Markdown.Tokens;

namespace Markdown;

public class MarkdownLexer : ILexer
{
private int position;
private readonly List<Token> tokens = [];

private readonly char[] escapedChars =
[
MarkdownSymbols.SharpChar, MarkdownSymbols.GroundChar, MarkdownSymbols.EscapeChar, MarkdownSymbols.NewLineChar
];

public List<Token> Tokenize(string input) => Tokenize(new MarkdownLexerInput(input));

private List<Token> Tokenize(MarkdownLexerInput input)
{
position = 0;
var nestingStack = new Stack<string>();
Copy link

@masssha1308 masssha1308 Dec 10, 2024

Choose a reason for hiding this comment

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

Лучше position сделать локальной переменной чтобы была одинаковая логика работы с position, nestingStack, input

Copy link
Author

Choose a reason for hiding this comment

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

Если position сделать локальной переменной, то возникнет несколько неудобств:

  1. Придется прокидывать текущую позицию практически во все функции, увеличится список параметров
  2. Сейчас логика сдвига позиции находится максимально близко к самим "действиям", т.е. как только мы совершили какое-то действие (например, спарсили текст), мы сразу сдвигаем позицию в том же методе парсинга. Если будем передавать position в метод параметром, то нам придется либо передавать его ссылкой (ref), либо возвращать из метода необходимый сдвиг или еще как-то ухищряться, чтобы правильно сдвинуть position

Поэтому мне кажется, что более красиво и аккуратно будет оставить position полем класса, а чтобы сделать логику работы более одинаковой, можно вынести стек тоже полем класса и не передавать его каждый раз параметром (с инпутом так поступить не можем, потому что получем его параметром в методе, вынести инициализацию в конструктор тоже не можем, потому что нарушим контракт метода Tokenize и не сможем получать в конвертере не конкретную реализацию лексера, а интерфейс)


while (position < input.Length)
{
switch (input[position])
{
case MarkdownSymbols.SpaceChar:
ParseSpaceAndAdvance();
break;
case MarkdownSymbols.NewLineChar:
ParseNewLineAndAdvance(nestingStack);
break;
case MarkdownSymbols.EscapeChar:
ParseEscapeAndAdvance(input);
break;
case MarkdownSymbols.GroundChar:
ParseItalicOrBoldAndAdvance(input, nestingStack);
break;
case MarkdownSymbols.SharpChar:
ParseHeadingAndAdvance(input);
break;
default:
ParseTextAndAdvance(input);
break;
}
}

return tokens;
}

private void ParseSpaceAndAdvance() => tokens.Add(new SpaceToken(position++));

private void ParseHeadingAndAdvance(MarkdownLexerInput input)
{
if (input.NextIsSpace(position) && input.IsStartOfParagraph(position)) tokens.Add(new HeadingToken(position++));
else tokens.Add(new TextToken(position, MarkdownSymbols.Sharp));
position++;
}

private void ParseTextAndAdvance(MarkdownLexerInput input)
{
var value = new StringBuilder();
var start = position;
var endChars = new[]
{
MarkdownSymbols.SharpChar, MarkdownSymbols.GroundChar, MarkdownSymbols.NewLineChar,
MarkdownSymbols.EscapeChar, MarkdownSymbols.SpaceChar
};
while (position < input.Length && !endChars.Contains(input[position]) && !input.CurrentIsDigit(position))
value.Append(input[position++]);

if (value.Length > 0) tokens.Add(new TextToken(start, value.ToString()));
if (position < input.Length && input.CurrentIsDigit(position)) ParseNumberAndAdvance(input);
}


private void ParseNumberAndAdvance(MarkdownLexerInput input)
{
var sb = new StringBuilder();
var start = position;
while (position < input.Length && (input.CurrentIsDigit(position) || input[position] == MarkdownSymbols.GroundChar))
sb.Append(input[position++]);
tokens.Add(new NumberToken(start, sb.ToString()));
}

private void ParseItalicOrBoldAndAdvance(MarkdownLexerInput input, Stack<string> stack)
{
var isDoubleGround = input.NextIsGround(position);
var isTripleGround = input.NextIsDoubleGround(position);
var isSingleGround = !isTripleGround && !isDoubleGround;
if (stack.Count == 0) ParseItalicOrBoldAndAdvanceWhenStackEmpty(isSingleGround, isTripleGround, stack);

Choose a reason for hiding this comment

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

можно заменить на switch

else if (stack.Count == 1)
ParseItalicOrBoldAndAdvanceWhenStackHasOne(isSingleGround, isDoubleGround, isTripleGround, stack);
else if (stack.Count == 2) ParseItalicOrBoldAndAdvanceWhenStackHasTwo(isSingleGround, isTripleGround, stack);
}

private void ParseItalicOrBoldAndAdvanceWhenStackEmpty(bool isSingleGround, bool isTripleGround,
Stack<string> stack)
{
if (isSingleGround)
{
ParseItalicAndAdvance();
stack.Push(MarkdownSymbols.Ground);
return;
}

ParseBoldAndAdvance();
stack.Push(MarkdownSymbols.DoubleGround);
if (!isTripleGround) return;
ParseItalicAndAdvance();
stack.Push(MarkdownSymbols.Ground);
}

private void ParseItalicOrBoldAndAdvanceWhenStackHasOne(bool isSingleGround, bool isDoubleGround,

Choose a reason for hiding this comment

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

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

Copy link
Author

Choose a reason for hiding this comment

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

На самом деле в стек всегда сразу добавляются корректные токены (открывающие токены в соответствии с разрешенной вложенностью), но трудность заключается в следующем: в процессе лексического анализа нам надо правильно (хотя бы частично) обрабатывать вложенность, чтобы на этапе парсинга получать уже "правильный" набор токенов (___text___ должен парситься лексером в ['__', '_', 'text', '_', '__'], а не, например, в ['_', '__', 'text', '_', '__']). Чтобы решать проблемы с вложенностью, я, по аналогии с популярной задачей про валидность скобочной последовательности, собирался просто закидывать в стек открывающие теги, а когда встречаем закрывающий, то достаем открывающий из стека, но из-за того, что тег курсива явялется частью тега полужирного, добавлять и доставать открывающие теги оказалось не так уж "просто" и приходится проверять очень много условий.

Изначально вся логика обработки этой вложенности была написана в ParseItalicOrBoldAndAdvance, но метод вырос до +-150 строк кода и очень глубокой вложенностью if-ов, и, чтобы хоть немного его уменьшить и сделать более декларативным я разнес эту логику по разным функциям, но получились такие страшные названия (называл прямо "в лоб", ровно то, что делает функция, возможно можно было назвать более удачно). В итоге я так и не придумал что-то лучше, чтобы код был более понятным и не нагруженным, мне показалось, что в текущем состоянии функции получились в достаточной мере декларативными, но с очень длинными названиями (от этого избавиться мне не удалось)

Choose a reason for hiding this comment

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

Кажется большинство проблем бы решилось и код упростился, если бы в стек клали не строки (двойное или одинарное подчеркивание), а char (только одинарные подчеркивания)

Copy link
Author

Choose a reason for hiding this comment

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

Кажется большинство проблем бы решилось и код упростился, если бы в стек клали не строки (двойное или одинарное подчеркивание), а char (только одинарные подчеркивания)

Возможно, подумаю над этим

bool isTripleGround,

Choose a reason for hiding this comment

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

вместо булевых флагов можно передавать int

Stack<string> stack)
{
switch (stack.Peek())
{
case MarkdownSymbols.DoubleGround when isSingleGround:
ParseItalicAndAdvance();
stack.Push(MarkdownSymbols.Ground);
break;
case MarkdownSymbols.DoubleGround:
{
if (isTripleGround) ParseItalicAndAdvance();
ParseBoldAndAdvance();
stack.Pop();
break;
}
case MarkdownSymbols.Ground:
{
if (isTripleGround)
{
ParseBoldAndAdvance();
ParseItalicAndAdvance();
}
else if (isDoubleGround)
{
tokens.Add(new TextToken(position, MarkdownSymbols.DoubleGround));
position += 2;
}
else ParseItalicAndAdvance();

stack.Pop();
break;
}
}
}

private void ParseItalicOrBoldAndAdvanceWhenStackHasTwo(bool isSingleGround, bool isTripleGround,
Stack<string> stack)
{
if (isSingleGround)
{
ParseItalicAndAdvance();
stack.Pop();
return;
}

if (isTripleGround) ParseItalicAndAdvance();
ParseBoldAndAdvance();

stack.Pop();
stack.Pop();
}

private void ParseBoldAndAdvance()
{
tokens.Add(new BoldToken(position));
position += 2;
}

private void ParseItalicAndAdvance()
{
tokens.Add(new ItalicToken(position));
position++;
}

private void ParseNewLineAndAdvance(Stack<string> stack)
{
tokens.Add(new NewLineToken(position));
stack.Clear();
position++;
}

private void ParseEscapeAndAdvance(MarkdownLexerInput input)
{
if (position + 1 >= input.Length)
{
tokens.Add(new TextToken(position++, MarkdownSymbols.Escape));
return;
}

if (input.NextIsDoubleGround(position))
{
tokens.Add(new TextToken(position, MarkdownSymbols.DoubleGround));
position += 3;
return;
}

var next = input[position + 1];
tokens.Add(escapedChars.Contains(next)
? new TextToken(position, next.ToString())
: new TextToken(position, MarkdownSymbols.Escape + next));
position += 2;
}
}
22 changes: 22 additions & 0 deletions cs/Markdown/MarkdownLexerInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Markdown;

public class MarkdownLexerInput(string input)
{
public bool NextIsDoubleGround(int position) =>
position + 2 < input.Length && input[position + 1] == MarkdownSymbols.GroundChar &&
input[position + 2] == MarkdownSymbols.GroundChar;

public bool NextIsSpace(int position) =>
position + 1 < input.Length && input[position + 1] == MarkdownSymbols.SpaceChar;

public bool NextIsGround(int position) =>
position + 1 < input.Length && input[position + 1] == MarkdownSymbols.GroundChar;

public bool CurrentIsDigit(int position) => char.IsDigit(input[position]);

public bool IsStartOfParagraph(int position) =>
position == 0 || position > 0 && input[position - 1] == MarkdownSymbols.NewLineChar;

public char this[int index] => input[index];
public int Length => input.Length;
}
Loading