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

Махлонов Дмитрий #222

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions MarkdownSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега \<strong>.

# Ссылки

\[текст ссылки в квадратных скобках\]\(https://google.com\) и сама ссылка в круглых скобках выделяется тегом \<a>.

# Экранирование

Expand Down
31 changes: 31 additions & 0 deletions cs/Markdown/Markdown.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown", "Markdown\Markdown.csproj", "{496ACCF1-011E-4C86-BB54-55A1E028805A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkdownTests", "MarkdownTests\MarkdownTests.csproj", "{63551462-1207-4447-A789-DADD036FA4CD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{496ACCF1-011E-4C86-BB54-55A1E028805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{496ACCF1-011E-4C86-BB54-55A1E028805A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{496ACCF1-011E-4C86-BB54-55A1E028805A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{496ACCF1-011E-4C86-BB54-55A1E028805A}.Release|Any CPU.Build.0 = Release|Any CPU
{63551462-1207-4447-A789-DADD036FA4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63551462-1207-4447-A789-DADD036FA4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63551462-1207-4447-A789-DADD036FA4CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63551462-1207-4447-A789-DADD036FA4CD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CFDB3D0E-41DB-4575-9CB6-FE8E02695109}
EndGlobalSection
EndGlobal
14 changes: 14 additions & 0 deletions cs/Markdown/Markdown/Markdown.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown;

public class BoldTokenHtmlConverter : HtmlTokenConverter
{
public BoldTokenHtmlConverter() : base(TokenType.Bold, "<strong>", "</strong>")
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown;

public class HeaderTokenHtmlConverter : HtmlTokenConverter
{
public HeaderTokenHtmlConverter() : base(TokenType.Header, "<h1>", "</h1>")
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Markdown;

public abstract class HtmlTokenConverter(TokenType typeOfToken, string openTag, string closeTag)
{
public readonly TokenType TypeOfToken = typeOfToken;
public readonly string OpenTag = openTag;
public readonly string CloseTag = closeTag;

public virtual string Convert(Token token)
{
if (token.Type != TypeOfToken)
throw new ArgumentException($"{nameof(TokenType)} should be {nameof(TypeOfToken)} but was {token.Type}");

return $"{OpenTag}{token.Content}{CloseTag}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown;

public class ItalicTokenHtmlConverter : HtmlTokenConverter
{
public ItalicTokenHtmlConverter() : base(TokenType.Italic, "<em>", "</em>")
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Markdown;

public class LinkTokenHtmlConverter : HtmlTokenConverter
{
public LinkTokenHtmlConverter() : base(TokenType.Link, "<a", "</a>")
{
}

public override string Convert(Token token)
{
if (token.Type != TypeOfToken)
throw new ArgumentException($"{nameof(TokenType)} should be {nameof(TypeOfToken)} but was {token.Type}");

if (token is TokenWithArgument linkToken)
return $"{OpenTag} href=\"{linkToken.Argument}\">{linkToken.Content}{CloseTag}";
throw new ArgumentException($"{nameof(Token)} should be {nameof(TokenWithArgument)}");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Markdown;

public class TextTokenHtmlConverter : HtmlTokenConverter
{
public TextTokenHtmlConverter() : base(TokenType.SimpleText, string.Empty, string.Empty)
{
}
}
6 changes: 6 additions & 0 deletions cs/Markdown/Markdown/MarkdownConverters/IMarkdownConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Markdown;

public interface IMarkdownConverter
{
public string Convert(List<Token> tokens);
}
44 changes: 44 additions & 0 deletions cs/Markdown/Markdown/MarkdownConverters/MarkdownToHtmlConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Text;

namespace Markdown;

public class MarkdownToHtmlConverter : IMarkdownConverter
{
private readonly Dictionary<TokenType, HtmlTokenConverter> typeToConverters;

public MarkdownToHtmlConverter(IEnumerable<HtmlTokenConverter> converters)
{
typeToConverters = converters.ToDictionary(c => c.TypeOfToken);
}

public string Convert(List<Token> tokens)
{
ArgumentNullException.ThrowIfNull(tokens);

var result = new StringBuilder();

foreach (var token in tokens)
result.Append(ConvertToken(token));

return result.ToString();
}

private string ConvertToken(Token token)
{
var converter = typeToConverters[token.Type];

if (token is not ComplexToken)
return converter.Convert(token);

var result = new StringBuilder();

result.Append(converter.OpenTag);
foreach (var child in token.Childs)
{
result.Append(ConvertToken(child));
}
result.Append(converter.CloseTag);

return result.ToString();
}
}
14 changes: 14 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/BoldTokenParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Markdown;

public class BoldTokenParser : TokenParser
{
public override string StartPositionSymbol => "__";

public override string EndPositionSymbol => "__";

public override TokenType ParsingType => TokenType.Bold;

protected override List<TokenParser> parsersForChild => [new ItalicTokenParser()];

protected override char? escapedSymbol => '_';
}
15 changes: 15 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/HeaderTokenParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Markdown;

public class HeaderTokenParser : TokenParser
{
public override string StartPositionSymbol => "# ";

public override string EndPositionSymbol => "\n";

public override TokenType ParsingType => TokenType.Header;

protected override List<TokenParser> parsersForChild =>
[new ItalicTokenParser(), new BoldTokenParser()];

protected override char? escapedSymbol => null;
}
6 changes: 6 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/IMarkdownParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Markdown;

public interface IMarkdownParser
{
public List<Token> ParseTextToTokens(string text);
}
13 changes: 13 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/ItalicTokenParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Markdown;

public class ItalicTokenParser : TokenParser
{
public override string StartPositionSymbol => "_";
public override string EndPositionSymbol => "_";

public override TokenType ParsingType => TokenType.Italic;

protected override List<TokenParser> parsersForChild => [];

protected override char? escapedSymbol => '_';
}
58 changes: 58 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/LinkTokenParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace Markdown;

public class LinkTokenParser : TokenParser
{
public override string StartPositionSymbol => "[";

public override string EndPositionSymbol => ")";

public override TokenType ParsingType => TokenType.Link;

protected override char? escapedSymbol => ')';

protected override List<TokenParser> parsersForChild => [];

public override ParseResult TryParseStringToToken(string value)
{
if (value.Length < StartPositionSymbol.Length + EndPositionSymbol.Length)
return ParseResult.NullResult;
var endPosition = FindEndPosition(value[StartPositionSymbol.Length..]);

if (!IsStartAndEndPositionCorrect(value, 0, endPosition))
return ParseResult.NullResult;

if (!IsTokenTextCorrect(value))
return ParseResult.NullResult;

var textAndArg = FindTextAndArgumentOfLink(value);
if (textAndArg == null)
return ParseResult.NullResult;
return new(
true,
ConvertToToken(textAndArg.Value.Item1, [], textAndArg.Value.Item2),
GetTokenLength(value[StartPositionSymbol.Length..endPosition]));
}

public (string, string)? FindTextAndArgumentOfLink(string value)
{
var endText = value.IndexOf(']');
var startArgument = value.IndexOf('(');
var endArgument = value.IndexOf(')');

if (!IsChildsPositionsCorrect(endText, startArgument, endArgument))
return null;

var text = value[1..endText];
var arg = value[(startArgument + 1)..endArgument];
if (text.Contains('[') || arg.Contains('('))
return null;

return (text, arg);
}

private bool IsChildsPositionsCorrect(int endText, int startArg, int endArg)
{
return !(endText == -1 || startArg == -1 || endArg == -1)
&& (endText + 1 == startArg);
}
}
79 changes: 79 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/MarkdownParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace Markdown;

public class MarkdownParser(List<TokenParser> parsers) : IMarkdownParser
{
private readonly List<TokenParser> parsers = parsers;

public List<Token> ParseTextToTokens(string text)
{
var tokens = new List<Token>();
var currentIndex = 0;
var endOfLastToken = 0;

do
{
var validParsers = parsers
.Where(p => p.IsSymbolCanBeStartOfToken(text[currentIndex]) && !IsSymbolEscaped(text, currentIndex));

if (!validParsers.Any())
{
currentIndex++;
continue;
}

var parseResult = validParsers
.Select(p => p.TryParseStringToToken(text[currentIndex..]))
.Where(t => t.HasToken).FirstOrDefault();

if (parseResult == null)
{
currentIndex++;
continue;
}

var lastSymbol = currentIndex + parseResult.TokenLength - 1;
if (IsSymbolEscaped(text, lastSymbol))
{
currentIndex++;
continue;
}

if (text[endOfLastToken..currentIndex].Length != 0)
tokens.Add(new SimpleToken(TokenType.SimpleText, text[endOfLastToken..currentIndex]));
tokens.Add(parseResult.Token);
currentIndex += parseResult.TokenLength;
endOfLastToken = currentIndex;
}
while (currentIndex < text.Length);

if (endOfLastToken != text.Length)
tokens.Add(new SimpleToken(TokenType.SimpleText, text[endOfLastToken..]));

return tokens;
}

private bool IsSymbolEscaped(string text, int indexOfSymbol)
{
if (indexOfSymbol == 0)
return false;
if (text[indexOfSymbol - 1] != '\\')
return false;

return !IsSymbolEscaped(text, indexOfSymbol - 1);
}

private int GetTokenLength(Token token)
{
if (token is not ComplexToken)
return token.Content.Length;

var result = 0;

foreach (var child in token.Childs)
{
result += GetTokenLength(child);
}

return result;
}
}
6 changes: 6 additions & 0 deletions cs/Markdown/Markdown/MarkdownParser/ParseResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Markdown;

public record class ParseResult(bool HasToken, Token? Token, int TokenLength)
{
public static ParseResult NullResult => new(false, null, -1);
}
Loading