diff --git a/MarkdownSpec.md b/MarkdownSpec.md
index 886e99c95..a8fab56c7 100644
--- a/MarkdownSpec.md
+++ b/MarkdownSpec.md
@@ -31,7 +31,15 @@ __Выделенный двумя символами текст__ должен
Символ экранирования тоже можно экранировать: \\_вот это будет выделено тегом_ \
+# Неупорядоченный список
+Элементы неупорядоченного списка начинаются с символа * за которым следует пробел.
+
+
+
+Внутри элементов неупорядоченного списока могут присутствовать все прочие символы разметки с указанными правилами, кроме заголовка.
# Взаимодействие тегов
diff --git a/cs/Markdown/Converter/ConcreteConverter/HtmlConverter.cs b/cs/Markdown/Converter/ConcreteConverter/HtmlConverter.cs
new file mode 100644
index 000000000..6bbc73442
--- /dev/null
+++ b/cs/Markdown/Converter/ConcreteConverter/HtmlConverter.cs
@@ -0,0 +1,51 @@
+using System.Text;
+using Markdown.Tags.ConcreteTags;
+
+namespace Markdown.Converter.ConcreteConverter;
+
+public class HtmlConverter : IConverter
+{
+ public string Convert(ParsedLine[] parsedLines)
+ {
+ var sb = new StringBuilder();
+
+ var containList = false;
+ var startedLine = true;
+ foreach (var text in parsedLines)
+ {
+ if (!startedLine)
+ sb.Append('\n');
+
+ if (containList && text.Tags.FirstOrDefault() is not BulletTag)
+ {
+ containList = false;
+ sb.Append("");
+ }
+ else if (!containList && text.Tags.FirstOrDefault() is BulletTag)
+ {
+ containList = true;
+ sb.Append("");
+ }
+
+ var prevTagPos = 0;
+ foreach (var tag in text.Tags)
+ {
+ sb.Append(text.Line.AsSpan(prevTagPos, tag.Position - prevTagPos));
+
+ sb.Append(tag.IsCloseTag ?
+ MdTagToHtmlConverter.CloseTags[tag.TagType] :
+ MdTagToHtmlConverter.OpenTags[tag.TagType]);
+
+ prevTagPos = tag.Position;
+ }
+
+ sb.Append(text.Line.AsSpan(prevTagPos, text.Line.Length - prevTagPos));
+ startedLine = false;
+ }
+
+ if (containList)
+ sb.Append("
");
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Converter/ConcreteConverter/MdTagToHtmlConverter.cs b/cs/Markdown/Converter/ConcreteConverter/MdTagToHtmlConverter.cs
new file mode 100644
index 000000000..4e75ebaa3
--- /dev/null
+++ b/cs/Markdown/Converter/ConcreteConverter/MdTagToHtmlConverter.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Markdown.Tags;
+using Markdown.Tags.ConcreteTags;
+
+namespace Markdown.Converter.ConcreteConverter
+{
+ public static class MdTagToHtmlConverter
+ {
+ public static Dictionary OpenTags = new()
+ {
+ { TagType.Header, ""},
+ { TagType.Bold, ""},
+ { TagType.BulletedListItem, ""},
+ { TagType.Italic, ""}
+ };
+
+ public static Dictionary CloseTags = new()
+ {
+ { TagType.Header, "
"},
+ { TagType.Bold, ""},
+ { TagType.BulletedListItem, ""},
+ { TagType.Italic, ""}
+ };
+ }
+}
diff --git a/cs/Markdown/Converter/IConverter.cs b/cs/Markdown/Converter/IConverter.cs
new file mode 100644
index 000000000..6a3825d67
--- /dev/null
+++ b/cs/Markdown/Converter/IConverter.cs
@@ -0,0 +1,6 @@
+namespace Markdown.Converter;
+
+public interface IConverter
+{
+ public string Convert(ParsedLine[] parsedLines);
+}
\ No newline at end of file
diff --git a/cs/Markdown/Extensions/StringExtensions.cs b/cs/Markdown/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..32b985447
--- /dev/null
+++ b/cs/Markdown/Extensions/StringExtensions.cs
@@ -0,0 +1,14 @@
+namespace Markdown.Extensions;
+
+public static class StringExtensions
+{
+ public static string[] SplitIntoLines(this string text)
+ {
+ return text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ }
+
+ public static bool NextCharIs(this string line, char ch, int currentIndex)
+ {
+ return currentIndex + 1 < line.Length && line[currentIndex + 1] == ch;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Extensions/TokenExtensions.cs b/cs/Markdown/Extensions/TokenExtensions.cs
new file mode 100644
index 000000000..4ef6b41a4
--- /dev/null
+++ b/cs/Markdown/Extensions/TokenExtensions.cs
@@ -0,0 +1,29 @@
+using Markdown.Tags;
+using Markdown.Tokens;
+
+namespace Markdown.Extensions;
+
+public static class TokenExtensions
+{
+ public static bool NextTokenIs(this List tokens, TokenType tokenType, int currentIndex)
+ {
+ return currentIndex + 1 < tokens.Count && tokens[currentIndex + 1].TokenType == tokenType;
+ }
+
+ public static bool CurrentTokenIs(this List tokens, TokenType tokenType, int currentIndex)
+ {
+ return currentIndex < tokens.Count && currentIndex >= 0 &&
+ tokens[currentIndex].TokenType == tokenType;
+ }
+
+ public static bool CurrentTokenIs(this List tokens, TagType tokenType, int currentIndex)
+ {
+ return currentIndex < tokens.Count && currentIndex >= 0 &&
+ tokens[currentIndex].TagType == tokenType;
+ }
+
+ public static bool LastTokenIs(this List tokens, TokenType tokenType, int currentIndex)
+ {
+ return currentIndex - 1 >= 0 && tokens[currentIndex - 1].TokenType == tokenType;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj
new file mode 100644
index 000000000..fa71b7ae6
--- /dev/null
+++ b/cs/Markdown/Markdown.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs
new file mode 100644
index 000000000..cc3780a63
--- /dev/null
+++ b/cs/Markdown/Md.cs
@@ -0,0 +1,24 @@
+using Markdown.Converter;
+using Markdown.Extensions;
+using Markdown.TokenParser.Interfaces;
+
+namespace Markdown;
+
+public class Md
+{
+ private readonly IConverter converter;
+ private readonly ITokenLineParser markdownTokenizer;
+
+ public Md(ITokenLineParser tokenizer, IConverter converter)
+ {
+ markdownTokenizer = tokenizer;
+ this.converter = converter;
+ }
+
+ public string Render(string mdString)
+ {
+ return converter.Convert(mdString.SplitIntoLines()
+ .Select(markdownTokenizer.ParseLine)
+ .ToArray());
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/ParsedLine.cs b/cs/Markdown/ParsedLine.cs
new file mode 100644
index 000000000..afba1888d
--- /dev/null
+++ b/cs/Markdown/ParsedLine.cs
@@ -0,0 +1,21 @@
+using Markdown.Tags;
+
+namespace Markdown;
+
+public class ParsedLine
+{
+ public readonly string Line;
+
+ public readonly List Tags;
+
+ public ParsedLine(string line, List tags)
+ {
+ if (line == null || tags == null)
+ throw new ArgumentNullException($"Параметры Line и Tags класса ParsedLine не могут быть null");
+ if (tags.Any(x => x.Position > line.Length))
+ throw new ArgumentException("Позиция тега не может быть больше длины строки", nameof(tags));
+
+ Line = line;
+ Tags = tags;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/ConcreteTags/BoldTag.cs b/cs/Markdown/Tags/ConcreteTags/BoldTag.cs
new file mode 100644
index 000000000..2f33b1fce
--- /dev/null
+++ b/cs/Markdown/Tags/ConcreteTags/BoldTag.cs
@@ -0,0 +1,16 @@
+namespace Markdown.Tags.ConcreteTags;
+
+public class BoldTag : ITag
+{
+ public BoldTag(int position, bool isCloseTag = false)
+ {
+ Position = position;
+ IsCloseTag = isCloseTag;
+ }
+
+ public TagType TagType => TagType.Bold;
+
+ public int Position { get; set; }
+
+ public bool IsCloseTag { get; set; }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/ConcreteTags/BulletTag.cs b/cs/Markdown/Tags/ConcreteTags/BulletTag.cs
new file mode 100644
index 000000000..886b9cf8c
--- /dev/null
+++ b/cs/Markdown/Tags/ConcreteTags/BulletTag.cs
@@ -0,0 +1,14 @@
+namespace Markdown.Tags.ConcreteTags;
+
+public class BulletTag : ITag
+{
+ public BulletTag(int position, bool isCloseTag = false)
+ {
+ Position = position;
+ IsCloseTag = isCloseTag;
+ }
+
+ public TagType TagType => TagType.BulletedListItem;
+ public int Position { get; set; }
+ public bool IsCloseTag { get; set; }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/ConcreteTags/HeaderTag.cs b/cs/Markdown/Tags/ConcreteTags/HeaderTag.cs
new file mode 100644
index 000000000..aec3de68d
--- /dev/null
+++ b/cs/Markdown/Tags/ConcreteTags/HeaderTag.cs
@@ -0,0 +1,16 @@
+namespace Markdown.Tags.ConcreteTags;
+
+public class HeaderTag : ITag
+{
+ public HeaderTag(int position, bool isCloseTag = false)
+ {
+ Position = position;
+ IsCloseTag = isCloseTag;
+ }
+
+ public TagType TagType => TagType.Header;
+
+ public int Position { get; set; }
+
+ public bool IsCloseTag { get; set; }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/ConcreteTags/ItalicTag.cs b/cs/Markdown/Tags/ConcreteTags/ItalicTag.cs
new file mode 100644
index 000000000..43d4aab79
--- /dev/null
+++ b/cs/Markdown/Tags/ConcreteTags/ItalicTag.cs
@@ -0,0 +1,16 @@
+namespace Markdown.Tags.ConcreteTags;
+
+public class ItalicTag : ITag
+{
+ public ItalicTag(int position, bool isCloseTag = false)
+ {
+ Position = position;
+ IsCloseTag = isCloseTag;
+ }
+
+ public TagType TagType => TagType.Italic;
+
+ public int Position { get; set; }
+
+ public bool IsCloseTag { get; set; }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/ITag.cs b/cs/Markdown/Tags/ITag.cs
new file mode 100644
index 000000000..8ac20edbd
--- /dev/null
+++ b/cs/Markdown/Tags/ITag.cs
@@ -0,0 +1,10 @@
+namespace Markdown.Tags;
+
+public interface ITag
+{
+ public TagType TagType { get; }
+
+ public int Position { get; protected set; }
+
+ public bool IsCloseTag { get; protected set; }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tags/TagType.cs b/cs/Markdown/Tags/TagType.cs
new file mode 100644
index 000000000..89d31d925
--- /dev/null
+++ b/cs/Markdown/Tags/TagType.cs
@@ -0,0 +1,10 @@
+namespace Markdown.Tags;
+
+public enum TagType
+{
+ Header,
+ Italic,
+ Bold,
+ BulletedListItem,
+ UnDefined
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/Interfaces/ITokenGenerateRule.cs b/cs/Markdown/TokenGeneratorClasses/Interfaces/ITokenGenerateRule.cs
new file mode 100644
index 000000000..1478b4e48
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/Interfaces/ITokenGenerateRule.cs
@@ -0,0 +1,8 @@
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.Interfaces;
+
+public interface ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex);
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/Interfaces/ITokenGenerator.cs b/cs/Markdown/TokenGeneratorClasses/Interfaces/ITokenGenerator.cs
new file mode 100644
index 000000000..9ee6cba86
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/Interfaces/ITokenGenerator.cs
@@ -0,0 +1,8 @@
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.Interfaces;
+
+public interface ITokenGenerator
+{
+ public static abstract Token? GetToken(string line, int currentIndex);
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGenerator.cs b/cs/Markdown/TokenGeneratorClasses/TokenGenerator.cs
new file mode 100644
index 000000000..e6d2c9c02
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGenerator.cs
@@ -0,0 +1,69 @@
+using System.Reflection;
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses;
+
+public class TokenGenerator : ITokenGenerator
+{
+ private static readonly IEnumerable generateRuleClasses = GetRuleClasses();
+
+ public static Token? GetToken(string line, int currentIndex)
+ {
+ foreach (var rule in generateRuleClasses)
+ {
+ var token = rule.GetToken(line, currentIndex);
+ if (token != null)
+ return token;
+ }
+
+ return null;
+ }
+
+ private static IEnumerable GetRuleClasses()
+ {
+ var interfaceType = typeof(ITokenGenerateRule);
+
+ var rulesTypes = Assembly.GetExecutingAssembly()
+ .GetTypes()
+ .Where(t => interfaceType.IsAssignableFrom(t) && t.IsClass)
+ .ToHashSet();
+
+ var simpleRules = GetRulesNotUsesOthersRules(rulesTypes).ToList();
+ var complexRules = GetRulesUsesOthersRulesLogic(rulesTypes, simpleRules).ToList();
+
+ return simpleRules.Concat(complexRules);
+ }
+
+ private static IEnumerable GetRulesNotUsesOthersRules(HashSet rulesTypes)
+ {
+ foreach (var type in rulesTypes)
+ {
+ var constructors = type.GetConstructors();
+
+ if (constructors.Length == 1 && constructors[0].GetParameters().Length == 0)
+ {
+ rulesTypes.Remove(type);
+ yield return (ITokenGenerateRule)Activator.CreateInstance(type);
+ }
+ }
+ }
+
+ private static IEnumerable GetRulesUsesOthersRulesLogic(HashSet rulesTypes,
+ IEnumerable rulesNotUsesOthersRules)
+ {
+ var getTokenFuncs = rulesNotUsesOthersRules
+ .Select(rule => new Func(rule.GetToken));
+
+ foreach (var type in rulesTypes)
+ {
+ var constructor = type.GetConstructor(new[] { typeof(IEnumerable>) });
+
+ if (constructor == null)
+ throw new ArgumentNullException("TokenGeneratorRules should have only one constructor " +
+ "without arguments or with IEnumerable argument");
+ rulesTypes.Remove(type);
+ yield return (ITokenGenerateRule)constructor.Invoke(new object[] { getTokenFuncs });
+ }
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateBoldTokenRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateBoldTokenRule.cs
new file mode 100644
index 000000000..9524002b1
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateBoldTokenRule.cs
@@ -0,0 +1,17 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateBoldTokenRule : ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex)
+ {
+ if (line[currentIndex] == '_' && line.NextCharIs('_', currentIndex))
+ return new Token(TokenType.MdTag, "__", currentIndex, false, TagType.Bold);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateBulletTokenRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateBulletTokenRule.cs
new file mode 100644
index 000000000..e1d0bb509
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateBulletTokenRule.cs
@@ -0,0 +1,16 @@
+using Markdown.Tags;
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateBulletTokenRule : ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex)
+ {
+ if (currentIndex + 1 < line.Length && line[currentIndex] == '*' && line[currentIndex + 1] == ' ')
+ return new Token(TokenType.MdTag, "* ", currentIndex, false, TagType.BulletedListItem);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateEscapeTokenRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateEscapeTokenRule.cs
new file mode 100644
index 000000000..393f875c6
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateEscapeTokenRule.cs
@@ -0,0 +1,15 @@
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateEscapeTokenRule : ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex)
+ {
+ if (line[currentIndex] == '\\')
+ return new Token(TokenType.Escape, @"\", currentIndex);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateHashTokenRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateHashTokenRule.cs
new file mode 100644
index 000000000..46b0b16d0
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateHashTokenRule.cs
@@ -0,0 +1,16 @@
+using Markdown.Tags;
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateHashTokenRule : ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex)
+ {
+ if (currentIndex + 1 < line.Length && line[currentIndex] == '#' && line[currentIndex + 1] == ' ')
+ return new Token(TokenType.MdTag, "# ", currentIndex, false, TagType.Header);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateItalicTokenRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateItalicTokenRule.cs
new file mode 100644
index 000000000..d9eb66496
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateItalicTokenRule.cs
@@ -0,0 +1,17 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateItalicTokenRule : ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex)
+ {
+ if (line[currentIndex] == '_' && !line.NextCharIs('_', currentIndex))
+ return new Token(TokenType.MdTag, "_", currentIndex, false, TagType.Italic);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateTextTokenRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateTextTokenRule.cs
new file mode 100644
index 000000000..99bcab547
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateTextTokenRule.cs
@@ -0,0 +1,50 @@
+using System.Text;
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateTextTokenRule : ITokenGenerateRule
+{
+ private readonly IEnumerable> otherTokensRules;
+
+ public GenerateTextTokenRule(IEnumerable> otherTokensRules)
+ {
+ this.otherTokensRules = otherTokensRules;
+ }
+
+ public Token? GetToken(string line, int currentIndex)
+ {
+ var stringBuilder = new StringBuilder();
+ var tokenType = char.IsNumber(line[currentIndex]) ? TokenType.Number : TokenType.Text;
+
+ for (var i = currentIndex; i < line.Length; i++)
+ {
+ if (tokenType == TokenType.Text &&
+ (char.IsNumber(line[currentIndex]) || !IsTextToken(line, currentIndex)))
+ break;
+
+ if (tokenType == TokenType.Number && !char.IsNumber(line[currentIndex]))
+ break;
+
+ stringBuilder.Append(line[currentIndex]);
+ currentIndex++;
+ }
+
+ var text = stringBuilder.ToString();
+
+ return new Token(tokenType, text, currentIndex - text.Length);
+ }
+
+ private bool IsTextToken(string line, int currentIndex)
+ {
+ foreach (var rule in otherTokensRules)
+ {
+ var token = rule.Invoke(line, currentIndex);
+ if (token != null)
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateWhiteSpaceRule.cs b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateWhiteSpaceRule.cs
new file mode 100644
index 000000000..84892ed08
--- /dev/null
+++ b/cs/Markdown/TokenGeneratorClasses/TokenGeneratorRules/GenerateWhiteSpaceRule.cs
@@ -0,0 +1,15 @@
+using Markdown.TokenGeneratorClasses.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenGeneratorClasses.TokenGeneratorRules;
+
+public class GenerateWhiteSpaceRule : ITokenGenerateRule
+{
+ public Token? GetToken(string line, int currentIndex)
+ {
+ if (line[currentIndex] == ' ')
+ return new Token(TokenType.WhiteSpace, " ", currentIndex);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/ConcreteParser/LineParser.cs b/cs/Markdown/TokenParser/ConcreteParser/LineParser.cs
new file mode 100644
index 000000000..171fe0c0a
--- /dev/null
+++ b/cs/Markdown/TokenParser/ConcreteParser/LineParser.cs
@@ -0,0 +1,251 @@
+using System.Text;
+using Markdown.Tags;
+using Markdown.Tags.ConcreteTags;
+using Markdown.TokenGeneratorClasses;
+using Markdown.TokenParser.Interfaces;
+using Markdown.TokenParser.TokenHandlers;
+using Markdown.Tokens;
+
+namespace Markdown.TokenParser.ConcreteParser;
+
+public class LineParser : ITokenLineParser
+{
+ private static readonly Dictionary TagPriority = new()
+ {
+ { TagType.Header, 1000 },
+ { TagType.BulletedListItem, 1000 },
+ { TagType.Bold, 500 },
+ { TagType.Italic, 300 },
+ { TagType.UnDefined, 0 }
+ };
+
+ public ParsedLine ParseLine(string line)
+ {
+ if (line is null)
+ throw new ArgumentNullException("String argument text must be not null");
+
+ var lineTokens = GetTokensLine(line);
+ var escapedTokens = ResetPositions(EscapeTags(lineTokens));
+ var headerTags = new HeaderTokensHandler().HandleLine(escapedTokens);
+ var bulletedLITags = new BulletedLIHandler().HandleLine(escapedTokens);
+ var italicTags = new ItalicTokensHandler().HandleLine(escapedTokens);
+ var boldTags = new BoldTokensHandler().HandleLine(escapedTokens);
+
+ var merged = MergeTokens(escapedTokens, headerTags, boldTags, italicTags, bulletedLITags);
+ ProcessTokensIntersecting(merged);
+ ProcessInnerTags(merged);
+
+ return GetTagsAndCleanText(merged);
+ }
+
+ private List GetTokensLine(string line)
+ {
+ var position = 0;
+ var result = new List();
+
+ while (position < line.Length)
+ {
+ var token = TokenGenerator.GetToken(line, position);
+ result.Add(token);
+ position += token.Content.Length;
+ }
+
+ return result;
+ }
+
+ private List EscapeTags(List tokens)
+ {
+ Token? previousToken = null;
+ var result = new List();
+
+ foreach (var token in tokens)
+ if (previousToken is { TokenType: TokenType.Escape })
+ {
+ if (token.TokenType is TokenType.MdTag or TokenType.Escape)
+ {
+ token.TokenType = TokenType.Text;
+ token.TagType = TagType.UnDefined;
+ previousToken = token;
+ result.Add(token);
+ }
+ else
+ {
+ previousToken.TokenType = TokenType.Text;
+ result.Add(previousToken);
+ result.Add(token);
+ previousToken = token;
+ }
+ }
+ else if (token.TokenType == TokenType.Escape)
+ {
+ previousToken = token;
+ }
+ else
+ {
+ result.Add(token);
+ previousToken = token;
+ }
+
+ if (previousToken is not { TokenType: TokenType.Escape })
+ return result;
+
+ previousToken.TokenType = TokenType.Text;
+ result.Add(previousToken);
+ return result;
+ }
+
+ private List ResetPositions(List tokens)
+ {
+ var position = 0;
+
+ foreach (var token in tokens)
+ {
+ token.Position = position;
+ position += token.Content.Length;
+ }
+
+ return tokens;
+ }
+
+ private List MergeTokens(List allTokens, params List[] tokenLists)
+ {
+ var positionMap = new Dictionary();
+
+ foreach (var token in allTokens) positionMap[token.Position] = token;
+
+ foreach (var tokenList in tokenLists)
+ foreach (var token in tokenList)
+ positionMap[token.Position] = token;
+
+ // Преобразуем словарь обратно в список
+ var combinedTokens = positionMap.Values.ToList();
+
+ var combindedTokens = combinedTokens.OrderBy(token => token.Position)
+ .ToList();
+
+ return combinedTokens;
+ }
+
+ private void ProcessTokensIntersecting(List tokens)
+ {
+ var stack = new Stack();
+ var process = new List();
+
+ foreach (var token in tokens)
+ if (token.TagType == TagType.Italic || token.TagType == TagType.Bold)
+ {
+ process.Add(token);
+ if (token.IsCloseTag)
+ {
+ stack.TryPeek(out var openToken);
+ if (openToken != null && !openToken.IsCloseTag
+ && openToken.TagType == token.TagType)
+ {
+ process.Remove(stack.Pop());
+ process.Remove(token);
+ }
+ else
+ {
+ stack.Push(token);
+ }
+ }
+ else
+ {
+ stack.Push(token);
+ }
+ }
+
+ foreach (var token in stack)
+ {
+ token.TagType = TagType.UnDefined;
+ token.TokenType = TokenType.Text;
+ }
+ }
+
+ private void ProcessInnerTags(List tokens)
+ {
+ Token handleToken = null;
+ var setAllToText = false;
+ var startIndex = -1;
+ var endIndex = -1;
+
+ for (var i = 0; i < tokens.Count; i++)
+ if (handleToken == null)
+ {
+ if (setAllToText)
+ ConvertToTextTags(startIndex, endIndex, tokens);
+
+ handleToken = tokens[i];
+ startIndex = i;
+ }
+ else if (TagPriority[tokens[i].TagType] > TagPriority[handleToken.TagType])
+ {
+ setAllToText = true;
+ }
+ else if (tokens[i].TagType == handleToken.TagType && tokens[i].IsCloseTag)
+ {
+ endIndex = i;
+ }
+ }
+
+ private void ConvertToTextTags(int startIndex, int endIndex, List tokens)
+ {
+ if (startIndex == -1 || endIndex == -1)
+ return;
+
+ for (var i = startIndex; i <= endIndex; i++)
+ if (tokens[i].TokenType == TokenType.MdTag)
+ tokens[i].TokenType = TokenType.Text;
+ }
+
+ private List ConvertToTextTags(List tokens)
+ {
+ var result = new List();
+
+ foreach (var token in tokens)
+ {
+ if (token.TokenType == TokenType.MdTag) token.TokenType = TokenType.Text;
+
+ result.Add(token);
+ }
+
+ return result;
+ }
+
+
+ private ParsedLine GetTagsAndCleanText(List tokens)
+ {
+ var result = new List();
+
+ var lineBuilder = new StringBuilder();
+ foreach (var token in tokens)
+ {
+ if (token.TokenType is not TokenType.MdTag)
+ {
+ lineBuilder.Append(token.Content);
+ continue;
+ }
+
+ result.Add(GetNewTag(token, lineBuilder.Length));
+ }
+
+ //Мы можем вернуть либо пустой ParsedLine если Count == 0 или "стандартно"
+ //заполненный, в ином случае если Length == 0 то значит внутри тегов пусто
+ //И их надо превратить в текст
+ return lineBuilder.Length > 0 || tokens.Count == 0
+ ? new ParsedLine(lineBuilder.ToString(), result)
+ : GetTagsAndCleanText(ConvertToTextTags(tokens));
+ }
+
+ private ITag GetNewTag(Token token, int position)
+ {
+ return token.TagType switch
+ {
+ TagType.Header => new HeaderTag(position, token.IsCloseTag),
+ TagType.Italic => new ItalicTag(position, token.IsCloseTag),
+ TagType.Bold => new BoldTag(position, token.IsCloseTag),
+ TagType.BulletedListItem => new BulletTag(position, token.IsCloseTag),
+ _ => throw new NotImplementedException()
+ };
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/Interfaces/ITokenHandler.cs b/cs/Markdown/TokenParser/Interfaces/ITokenHandler.cs
new file mode 100644
index 000000000..587dcd153
--- /dev/null
+++ b/cs/Markdown/TokenParser/Interfaces/ITokenHandler.cs
@@ -0,0 +1,8 @@
+using Markdown.Tokens;
+
+namespace Markdown.TokenParser.Interfaces;
+
+public interface ITokenHandler
+{
+ List HandleLine(List line);
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/Interfaces/ITokenLineParser.cs b/cs/Markdown/TokenParser/Interfaces/ITokenLineParser.cs
new file mode 100644
index 000000000..d63837cb9
--- /dev/null
+++ b/cs/Markdown/TokenParser/Interfaces/ITokenLineParser.cs
@@ -0,0 +1,6 @@
+namespace Markdown.TokenParser.Interfaces;
+
+public interface ITokenLineParser
+{
+ public ParsedLine ParseLine(string text);
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/TokenHandlers/BoldTokensHandler.cs b/cs/Markdown/TokenParser/TokenHandlers/BoldTokensHandler.cs
new file mode 100644
index 000000000..ea1f2581a
--- /dev/null
+++ b/cs/Markdown/TokenParser/TokenHandlers/BoldTokensHandler.cs
@@ -0,0 +1,10 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.TokenParser.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenParser.TokenHandlers;
+
+public class BoldTokensHandler() : UnderscoreTokensHandler(TagType.Bold, "__")
+{
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/TokenHandlers/BulletedLIHandler.cs b/cs/Markdown/TokenParser/TokenHandlers/BulletedLIHandler.cs
new file mode 100644
index 000000000..be2689045
--- /dev/null
+++ b/cs/Markdown/TokenParser/TokenHandlers/BulletedLIHandler.cs
@@ -0,0 +1,48 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.Tokens;
+
+namespace Markdown.TokenParser.TokenHandlers;
+
+public class BulletedLIHandler
+{
+ public List HandleLine(List line)
+ {
+ var result = new List();
+ var position = 0;
+ var addCloseTag = false;
+
+ for (var j = 0; j < line.Count; j++)
+ {
+ if (IsBulletedListItem(line, j) && line[j].TagType == TagType.BulletedListItem)
+ {
+ result.Add(new Token(TokenType.MdTag, "* ",
+ position, false, TagType.BulletedListItem));
+
+ addCloseTag = true;
+ }
+ else if (line[j].TagType == TagType.BulletedListItem)
+ {
+ result.Add(new Token(TokenType.Text, "* ", position));
+ }
+
+ position += line[j].Content.Length;
+ }
+
+ if (addCloseTag)
+ result.Add(CreateCloseTag(position));
+
+
+ return result;
+ }
+
+ private bool IsBulletedListItem(List tokens, int index)
+ {
+ return index == 0 && tokens.CurrentTokenIs(TagType.BulletedListItem, index);
+ }
+
+ private Token CreateCloseTag(int lastIndex)
+ {
+ return new Token(TokenType.MdTag, "", lastIndex + 1, true, TagType.BulletedListItem);
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/TokenHandlers/HeaderTokensHandler.cs b/cs/Markdown/TokenParser/TokenHandlers/HeaderTokensHandler.cs
new file mode 100644
index 000000000..1a5a596a3
--- /dev/null
+++ b/cs/Markdown/TokenParser/TokenHandlers/HeaderTokensHandler.cs
@@ -0,0 +1,49 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.TokenParser.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenParser.TokenHandlers;
+
+public class HeaderTokensHandler : ITokenHandler
+{
+ public List HandleLine(List line)
+ {
+ var result = new List();
+ var position = 0;
+ var addCloseTag = false;
+
+ for (var j = 0; j < line.Count; j++)
+ {
+ if (IsHeader(line, j) && line[j].TagType == TagType.Header)
+ {
+ result.Add(new Token(TokenType.MdTag, "# ",
+ position, false, TagType.Header));
+
+ addCloseTag = true;
+ }
+ else if (line[j].TagType == TagType.Header)
+ {
+ result.Add(new Token(TokenType.Text, "# ", position));
+ }
+
+ position += line[j].Content.Length;
+ }
+
+ if (addCloseTag)
+ result.Add(CreateCloseTag(position));
+
+
+ return result;
+ }
+
+ private bool IsHeader(List tokens, int index)
+ {
+ return index == 0 && tokens.CurrentTokenIs(TagType.Header, index);
+ }
+
+ private Token CreateCloseTag(int lastIndex)
+ {
+ return new Token(TokenType.MdTag, "", lastIndex + 1, true, TagType.Header);
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/TokenHandlers/ItalicTokensHandler.cs b/cs/Markdown/TokenParser/TokenHandlers/ItalicTokensHandler.cs
new file mode 100644
index 000000000..aa45d6a98
--- /dev/null
+++ b/cs/Markdown/TokenParser/TokenHandlers/ItalicTokensHandler.cs
@@ -0,0 +1,10 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.TokenParser.Interfaces;
+using Markdown.Tokens;
+
+namespace Markdown.TokenParser.TokenHandlers;
+
+public class ItalicTokensHandler() : UnderscoreTokensHandler(TagType.Italic, "_")
+{
+}
\ No newline at end of file
diff --git a/cs/Markdown/TokenParser/TokenHandlers/UnderscoreTokensHandler.cs b/cs/Markdown/TokenParser/TokenHandlers/UnderscoreTokensHandler.cs
new file mode 100644
index 000000000..0aa557a94
--- /dev/null
+++ b/cs/Markdown/TokenParser/TokenHandlers/UnderscoreTokensHandler.cs
@@ -0,0 +1,168 @@
+using Markdown.Extensions;
+using Markdown.Tags;
+using Markdown.Tokens;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Markdown.TokenParser.TokenHandlers
+{
+ public abstract class UnderscoreTokensHandler(TagType tagType, string TokenContent)
+ {
+ private readonly TagType TagType = tagType;
+
+ private readonly string Content = TokenContent;
+
+ public List HandleLine(List line)
+ {
+ var result = new List();
+ var opened = new List(1);
+ var position = 0;
+ Func, int, bool> isClosed = (list, i) => false;
+
+ for (var j = 0; j < line.Count; j++)
+ {
+ if (opened.Count == 0 && IsOpen(j, line))
+ {
+ if (IsOpenInWord(j, line))
+ {
+ var token = line[j];
+ isClosed = (list, index) => IsCloseInWord(index, list, token);
+ opened.Add(line[j]);
+ }
+ else if (IsOpenBetweenWords(j, line))
+ {
+ var token = line[j];
+ isClosed = (list, i) => IsClosed(i, list, token);
+ opened.Add(line[j]);
+ }
+ }
+ else if (opened.Count > 0 && isClosed(line, j))
+ {
+ result.Add(opened[0]);
+ result.Add(new Token(TokenType.MdTag, TokenContent,
+ position, true, TagType));
+
+ opened.Clear();
+ }
+ else if (line[j].TagType == TagType)
+ {
+ result.Add(new Token(TokenType.Text, TokenContent, position));
+ }
+
+ position += line[j].Content.Length;
+ }
+
+ if (opened.Count > 0)
+ {
+ opened[0].TagType = TagType.UnDefined;
+ opened[0].TokenType = TokenType.Text;
+
+ result.Add(opened[0]);
+ }
+
+ return result;
+ }
+
+ private bool IsOpen(int index, List tokens)
+ {
+ var isFirstInLine = IsFirstInLine(index, tokens);
+ var isOpenOrdinary = IsOpenOrdinary(index, tokens);
+ var isOpenClosed = IsInWord(index, tokens);
+
+ return isFirstInLine || isOpenOrdinary || isOpenClosed;
+ }
+
+ private bool IsClosed(int index, List tokens, Token token)
+ {
+ var isLastInLine = IsLastInLine(index, tokens);
+ var isClosedOrdinary = IsClosedOrdinary(index, tokens);
+ var isOpenClosed = IsCloseInWord(index, tokens, token);
+
+ return isLastInLine || isClosedOrdinary || isOpenClosed;
+ }
+
+ private bool IsOpenBetweenWords(int index, List tokens)
+ {
+ return IsOpenOrdinary(index, tokens) ^ IsFirstInLine(index, tokens);
+ }
+
+ private bool IsOpenInWord(int index, List tokens)
+ {
+ return IsInWord(index, tokens);
+ }
+
+ private bool IsCloseBetweenWords(int index, List tokens)
+ {
+ return IsClosedOrdinary(index, tokens) ^ IsLastInLine(index, tokens);
+ }
+
+ private bool IsCloseInWord(int index, List tokens, Token openToken)
+ {
+ return index - 2 > -1 && (IsInWord(index, tokens) || IsCloseBetweenWords(index, tokens))
+ && tokens[index - 2] == openToken;
+ }
+
+ #region OpenSituations
+
+ ///
+ /// Определяет является ли токен одновременно и
+ /// открывающим и закрывающим тегом - случай
+ /// если тэг в слове ("пре_сп_ко_йн_ый)
+ ///
+ /// Индекс токена
+ /// Список токенов для проверки
+ /// true если тег внутри слова иначе false
+ private bool IsInWord(int index, List tokens)
+ {
+ return tokens.LastTokenIs(TokenType.Text, index) &&
+ tokens.CurrentTokenIs(TagType, index) &
+ tokens.NextTokenIs(TokenType.Text, index);
+ }
+
+ private bool IsFirstInLine(int index, List tokens)
+ {
+ return index == 0 && tokens.CurrentTokenIs(TagType, index) &&
+ (tokens.NextTokenIs(TokenType.Text, index) ||
+ tokens.NextTokenIs(TokenType.MdTag, index));
+ }
+
+ private bool IsOpenOrdinary(int index, List tokens)
+ {
+ var hasWhSpaceBefore = tokens.LastTokenIs(TokenType.WhiteSpace, index);
+ var hasMdTagBefore = tokens.LastTokenIs(TokenType.MdTag, index);
+
+ return (hasWhSpaceBefore || hasMdTagBefore) &&
+ tokens.CurrentTokenIs(TagType, index) &&
+ tokens.NextTokenIs(TokenType.Text, index);
+ }
+
+ #endregion
+
+
+ #region CloseSituations
+
+ private bool IsLastInLine(int index, List tokens)
+ {
+ var isLast = index == tokens.Count() - 1;
+
+ return isLast && tokens.CurrentTokenIs(TagType, index) &&
+ (tokens.LastTokenIs(TokenType.Text, index) ||
+ tokens.LastTokenIs(TokenType.MdTag, index));
+ }
+
+ private bool IsClosedOrdinary(int index, List tokens)
+ {
+ var hasTextAfter = tokens.NextTokenIs(TokenType.WhiteSpace, index);
+ var hasMdTagAfter = tokens.NextTokenIs(TokenType.MdTag, index);
+
+ return tokens.LastTokenIs(TokenType.Text, index) &&
+ tokens.CurrentTokenIs(TagType, index) &&
+ (hasMdTagAfter || hasTextAfter);
+ }
+
+ #endregion
+ }
+}
diff --git a/cs/Markdown/Tokens/Token.cs b/cs/Markdown/Tokens/Token.cs
new file mode 100644
index 000000000..3c2e07cd4
--- /dev/null
+++ b/cs/Markdown/Tokens/Token.cs
@@ -0,0 +1,63 @@
+using Markdown.Tags;
+
+namespace Markdown.Tokens;
+
+public class Token
+{
+ public Token(TokenType tokenType, string content, int position, bool isCloseTag = false,
+ TagType tagType = TagType.UnDefined)
+ {
+ TokenType = tokenType;
+ Content = content;
+ Position = position;
+ IsCloseTag = isCloseTag;
+ TagType = tagType;
+ }
+
+ public TokenType TokenType { get; set; }
+ public string Content { get; set; }
+ public TagType TagType { get; set; }
+ public bool IsCloseTag { get; set; }
+ public int Position { get; set; }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is null) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((Token)obj);
+ }
+
+ public bool Equals(Token other)
+ {
+ if (other is null) return false;
+ return TokenType == other.TokenType &&
+ Content == other.Content &&
+ TagType == other.TagType &&
+ IsCloseTag == other.IsCloseTag &&
+ Position == other.Position;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked // Overflow is fine, just wrap
+ {
+ var hashCode = (int)TokenType;
+ hashCode = (hashCode * 397) ^ (Content != null ? Content.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (int)TagType;
+ hashCode = (hashCode * 397) ^ IsCloseTag.GetHashCode();
+ hashCode = (hashCode * 397) ^ Position;
+ return hashCode;
+ }
+ }
+
+ public static bool operator ==(Token left, Token right)
+ {
+ return EqualityComparer.Default.Equals(left, right);
+ }
+
+ public static bool operator !=(Token left, Token right)
+ {
+ return !(left == right);
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown/Tokens/TokenType.cs b/cs/Markdown/Tokens/TokenType.cs
new file mode 100644
index 000000000..fe36d922b
--- /dev/null
+++ b/cs/Markdown/Tokens/TokenType.cs
@@ -0,0 +1,10 @@
+namespace Markdown.Tokens;
+
+public enum TokenType
+{
+ MdTag,
+ Text,
+ Number,
+ Escape,
+ WhiteSpace
+}
\ No newline at end of file
diff --git a/cs/Markdown_Tests/LineParser_Tests.cs b/cs/Markdown_Tests/LineParser_Tests.cs
new file mode 100644
index 000000000..f9a7c75c5
--- /dev/null
+++ b/cs/Markdown_Tests/LineParser_Tests.cs
@@ -0,0 +1,81 @@
+using FluentAssertions;
+using Markdown.Tags;
+using Markdown.TokenParser.ConcreteParser;
+using Markdown.TokenParser.Interfaces;
+using MarkdownTests.TestData;
+
+namespace MarkdownTests
+{
+ public class LineParserTests
+ {
+ private ITokenLineParser parser = new LineParser();
+
+ [Test]
+ public void ParseLine_ThrowArgumentNullException_WhenArgumentIsNull()
+ {
+ Assert.Throws(() => parser.ParseLine(null), "String argument text must be not null");
+ }
+
+ [Test]
+ public void ParseLine_ShouldBeEmpty_WhenArgumentStringIsEmpty()
+ {
+ var parsedLine = parser.ParseLine(String.Empty);
+ parsedLine.Line.Should().BeEmpty();
+ parsedLine.Tags.Should().BeEmpty();
+ }
+
+ [TestCaseSource(typeof(LineParserData), nameof(LineParserData.WordsOnlyLines))]
+ public void ParseLine_ShoudBeCorrect_WhenLineWithWordsOnly(string inLine, string expectedLine, List tags)
+ {
+ var parsedLines = parser.ParseLine(inLine);
+
+ parsedLines.Line.Should().BeEquivalentTo(expectedLine);
+ parsedLines.Tags.Should().BeEquivalentTo(tags);
+ }
+
+ [TestCaseSource(typeof(LineParserData), nameof(LineParserData.LineWithHeader))]
+ public void ParseLine_ShoudBeCorrect_WhenLineWithHeaderTags(string inLine, string expectedLine, List tags)
+ {
+ var parsedLines = parser.ParseLine(inLine);
+
+ parsedLines.Line.Should().BeEquivalentTo(expectedLine);
+ parsedLines.Tags.Should().BeEquivalentTo(tags);
+ }
+
+ [TestCaseSource(typeof(LineParserData), nameof(LineParserData.LinesWithBulletedList))]
+ public void ParseLine_ShoudBeCorrect_WhenLinesWithBulletedList(string inLine, string expectedLine, List tags)
+ {
+ var parsedLines = parser.ParseLine(inLine);
+
+ parsedLines.Line.Should().BeEquivalentTo(expectedLine);
+ parsedLines.Tags.Should().BeEquivalentTo(tags);
+ }
+
+ [TestCaseSource(typeof(LineParserData), nameof(LineParserData.LineWithItalic))]
+ public void ParseLine_ShoudBeCorrect_WhenLineWithItalicTags(string inLine, string expectedLine, List tags)
+ {
+ var parsedLines = parser.ParseLine(inLine);
+
+ parsedLines.Line.Should().BeEquivalentTo(expectedLine);
+ parsedLines.Tags.Should().BeEquivalentTo(tags);
+ }
+
+ [TestCaseSource(typeof(LineParserData), nameof(LineParserData.LineWithBold))]
+ public void ParseLine_ShoudBeCorrect_WhenLineWithBoldTags(string inLine, string expectedLine, List tags)
+ {
+ var parsedLines = parser.ParseLine(inLine);
+
+ parsedLines.Line.Should().BeEquivalentTo(expectedLine);
+ parsedLines.Tags.Should().BeEquivalentTo(tags);
+ }
+
+ [TestCaseSource(typeof(LineParserData), nameof(LineParserData.MultiTagsLine))]
+ public void ParseLine_ShoudBeCorrect_WhenLineWithMultiTags(string inLine, string expectedLine, List tags)
+ {
+ var parsedLines = parser.ParseLine(inLine);
+
+ parsedLines.Line.Should().BeEquivalentTo(expectedLine);
+ parsedLines.Tags.Should().BeEquivalentTo(tags);
+ }
+ }
+}
diff --git a/cs/Markdown_Tests/MarkdownTests.csproj b/cs/Markdown_Tests/MarkdownTests.csproj
new file mode 100644
index 000000000..25f0ec504
--- /dev/null
+++ b/cs/Markdown_Tests/MarkdownTests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cs/Markdown_Tests/Md_Tests.cs b/cs/Markdown_Tests/Md_Tests.cs
new file mode 100644
index 000000000..dd18aad65
--- /dev/null
+++ b/cs/Markdown_Tests/Md_Tests.cs
@@ -0,0 +1,221 @@
+using FluentAssertions;
+using Markdown;
+using Markdown.Converter.ConcreteConverter;
+using Markdown.TokenParser.ConcreteParser;
+
+
+namespace MarkdownTests
+{
+ public class MdTests
+ {
+ private Md markdown = new Md(new LineParser(), new HtmlConverter());
+
+ #region BulletedTests
+
+ [TestCase("* \n* ", "")]
+ [TestCase("* ", "")]
+ public void Md_ShouldCreateBulletedListCorrectly_WhenBulletedItemNotEscaped(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"\* ", "* ")]
+ [TestCase("* \n\\* ", "* ")]
+ public void Md_ShouldCreateBulletedListCorrectly_WhenBulletedItemEscaped(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(" * \n * ", " * \n * ")]
+ [TestCase("d* \nd* ", "d* \nd* ")]
+ [TestCase("* \nd* ", "d* ")]
+ public void Md_ShouldNotCreateBulletedTag_WhenAreCharsBeforeTag(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ #endregion
+
+ #region HeaderTests
+
+ [TestCase(@"# bibo", "bibo
")]
+ [TestCase(@"# # bibo", "# bibo
")]
+ public void Md_ShouldCreateHeaderCorrectly_WhenHeaderNotEscaped(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"\# bibo", @"# bibo")]
+ [TestCase(@"\# # bibo", @"# # bibo")]
+ public void Md_ShouldNotCreateHeaderTag_WhenHeaderEscaped(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"\\# bibo", @"\# bibo")]
+ [TestCase(@"a # # bibo", @"a # # bibo")]
+ [TestCase(@" # bibo", " # bibo")]
+ public void Md_ShouldNotCreateHeaderTag_WhenAreCharsBeforeHash(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ #endregion
+
+ #region ItalicTests
+
+ [TestCase(@"_bibo love bubu_", @"bibo love bubu")]
+ [TestCase(@"bibo _love_ bubu", @"bibo love bubu")]
+ public void Md_ShouldCreateItalicTag_WhenTagAfterWhiteSpace(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"_ bibo love bubu_", @"_ bibo love bubu_")]
+ [TestCase(@"bibo _love _ bubu", @"bibo _love _ bubu")]
+ [TestCase(@"bibo _ love _ bubu", @"bibo _ love _ bubu")]
+ public void Md_ShouldNotCreateItalicTag_WhenWhiteSpaceBeforeOrAfterText(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"bibo _lo_ve bubu", @"bibo love bubu")]
+ [TestCase(@"bibo l_ove_ bubu", @"bibo love bubu")]
+ public void Md_ShouldCreateItalicTag_WhenTagInsideTextWithoutDigits(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+
+ [TestCase(@"bibo _love bu_bu", @"bibo _love bu_bu")]
+ [TestCase(@"bibo l_ove bu_bu", @"bibo l_ove bu_bu")]
+ [TestCase(@"bibo l_ove bubu_", @"bibo l_ove bubu_")]
+ public void Md_ShouldNotCreateItalicTag_WhenTagInsideTextInDifferentWords(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"bibo \_love_ bubu", @"bibo _love_ bubu")]
+ [TestCase(@"bibo _love\_ bubu", @"bibo _love_ bubu")]
+ [TestCase(@"bibo \_love\_ bubu", @"bibo _love_ bubu")]
+ public void Md_ShouldNotCreateItalicTag_WhenTagEscaped(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [Test]
+ public void Md_ShoudNotCreateItalicAndBoldTags_WhenBoldTagsInsideItalic()
+ {
+ var input = "I _want to __sleep__ tonight_";
+ var expected = "I want to sleep tonight";
+
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@" c _12_3 ", @" c _12_3 ")]
+ [TestCase(@"bibo love_4_ bubu", @"bibo love_4_ bubu")]
+ public void Md_ShouldNotCreateItalicTag_WhenTextInsideHaveDigits(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ #endregion
+
+ #region BoldTests
+
+ [TestCase(@"__bibo love bubu__", @"bibo love bubu")]
+ [TestCase(@"bibo __love__ bubu", @"bibo love bubu")]
+ public void Md_ShouldCreateBoldTag_WhenTagAfterSpace(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"__ bibo love bubu__", @"__ bibo love bubu__")]
+ [TestCase(@"bibo __love __ bubu", @"bibo __love __ bubu")]
+ [TestCase(@"bibo __ love __ bubu", @"bibo __ love __ bubu")]
+ public void Md_ShouldNotCreateBoldTag_WhenWhiteSpaceBeforeOrAfterText(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"bibo \__love__ bubu", @"bibo __love__ bubu")]
+ [TestCase(@"bibo __love\__ bubu", @"bibo __love__ bubu")]
+ [TestCase(@"bibo \__love\__ bubu", @"bibo __love__ bubu")]
+ public void Md_ShouldNotCreateBoldTag_WhenTagEscaped(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"bibo __lo__ve bubu", @"bibo love bubu")]
+ [TestCase(@"bibo l__ove__ bubu", @"bibo love bubu")]
+ public void Md_ShouldCreateBoldTag_WhenTagInsideTextWithoutDigits(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@"bibo __love bu__bu", @"bibo __love bu__bu")]
+ [TestCase(@"bibo l__ove bu__bu", @"bibo l__ove bu__bu")]
+ public void Md_ShouldNotCreateBoldTag_WhenTagInsideTextInDifferentWords(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [Test]
+ public void Md_ShoudCreateItalicAndBoldTags_WhenItalicTagsInsideBold()
+ {
+ var input = "I __want to _sleep_ tonight__";
+ var expected = "I want to sleep tonight";
+
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ [TestCase(@" c __12__3 ", @" c __12__3 ")]
+ [TestCase(@"bibo love__4__ bubu", @"bibo love__4__ bubu")]
+ public void Md_ShouldNotCreateBoldTag_WhenTextInsideHaveDigits(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ #endregion
+
+ #region IntersectionTest
+
+ [TestCase(@"__bibo _love__ bubu_", @"__bibo _love__ bubu_")]
+ [TestCase(@"_bibo __love_ bubu__", @"_bibo __love_ bubu__")]
+ public void Md_ShouldProccessIntersectsCorrecttly(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ #endregion
+
+ [TestCase(@"____", @"____")]
+ [TestCase(@"__", @"__")]
+ [TestCase(@"# ", @"# ")]
+ [TestCase(@"* ", @"* ")]
+ public void Md_ShouldNotCreateHtmlTags_WhenMdTagsContainsNothing(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+
+ //[Test]
+ public void Md_ShouldCreateEmptyHtml_WhenTextIsStringEmpty()
+ {
+ markdown.Render(String.Empty).Should().BeEquivalentTo(String.Empty);
+ }
+
+ public static IEnumerable MultiLinesTestCases()
+ {
+ yield return new TestCaseData("* _ _\n* __ __",
+ "");
+ yield return new TestCaseData("# \n* _ _\n* __ __",
+ "\n");
+ }
+
+ [TestCaseSource(nameof(MultiLinesTestCases))]
+ public void Md_ShouldRenderCorrectly_WhenTextWithMultiTags(string input, string expected)
+ {
+ markdown.Render(input).Should().BeEquivalentTo(expected);
+ }
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown_Tests/TestData/LineParserData.cs b/cs/Markdown_Tests/TestData/LineParserData.cs
new file mode 100644
index 000000000..084d3c09a
--- /dev/null
+++ b/cs/Markdown_Tests/TestData/LineParserData.cs
@@ -0,0 +1,179 @@
+using Markdown.Tags;
+using Markdown.Tags.ConcreteTags;
+
+namespace MarkdownTests.TestData
+{
+ public static class LineParserData
+ {
+ public static IEnumerable WordsOnlyLines()
+ {
+ yield return new TestCaseData("word bubo bibo", "word bubo bibo", new List());
+ yield return new TestCaseData("Why1 word 23 bubo bibo", "Why1 word 23 bubo bibo", new List());
+ }
+
+ public static IEnumerable LineWithHeader()
+ {
+ yield return new TestCaseData("# word bubo bibo", "word bubo bibo", new List()
+ {
+ new HeaderTag(0, false),
+ new HeaderTag(14, true),
+ });
+ yield return new TestCaseData("# Why1 word 23 bubo bibo", "Why1 word 23 bubo bibo", new List()
+ {
+ new HeaderTag(0, false),
+ new HeaderTag(22, true),
+ });
+ }
+
+ public static IEnumerable LineWithItalic()
+ {
+ yield return new TestCaseData("_word bubo_ bibo", "word bubo bibo", new List()
+ {
+ new ItalicTag(0, false),
+ new ItalicTag(9, true),
+ });
+ yield return new TestCaseData("_word bu_bo bibo", "_word bu_bo bibo", new List()
+ {
+ });
+ yield return new TestCaseData("wo_rd bubo_ bibo", "wo_rd bubo_ bibo", new List()
+ {
+ });
+ yield return new TestCaseData("wo_rd bu_bo bibo", "wo_rd bu_bo bibo", new List()
+ {
+ });
+ yield return new TestCaseData("_wo_rd bubo bibo", "word bubo bibo", new List()
+ {
+ new ItalicTag(0, false),
+ new ItalicTag(2, true),
+ });
+ yield return new TestCaseData("_word __bubo__ bibo_", "word bubo bibo", new List()
+ {
+ new ItalicTag(0, false),
+ new BoldTag(5, false),
+ new BoldTag(9, true),
+ new ItalicTag(14, true),
+ });
+ yield return new TestCaseData("l_ov_e", "love", new List()
+ {
+ new ItalicTag(1, false),
+ new ItalicTag(3, true),
+ });
+ yield return new TestCaseData("l_ove_", "love", new List()
+ {
+ new ItalicTag(1, false),
+ new ItalicTag(4, true),
+ });
+ yield return new TestCaseData("_word __bubo_ bibo__", "_word __bubo_ bibo__", new List());
+ yield return new TestCaseData(@"\_word bubo_ bibo", "_word bubo_ bibo", new List());
+ yield return new TestCaseData(@"_word bubo\_ bibo", "_word bubo_ bibo", new List());
+ yield return new TestCaseData(@"\_word bubo\_ bibo", "_word bubo_ bibo", new List());
+ yield return new TestCaseData("Why_1_ word 23 bubo bibo", "Why_1_ word 23 bubo bibo", new List());
+ }
+
+ public static IEnumerable LinesWithBulletedList()
+ {
+ yield return new TestCaseData("* один", "один", new List()
+ {
+ new BulletTag(0, false),
+ new BulletTag(4, true),
+ });
+ yield return new TestCaseData("* _один_ __два__", "один два", new List()
+ {
+ new BulletTag(0, false),
+ new ItalicTag(0, false),
+ new ItalicTag(4, true),
+ new BoldTag(5, false),
+ new BoldTag(8, true),
+ new BulletTag(8, true),
+ });
+ yield return new TestCaseData(@"\* один", "* один", new List());
+ yield return new TestCaseData(" * один", " * один", new List());
+ yield return new TestCaseData("горит * волбу", "горит * волбу", new List());
+ }
+
+ public static IEnumerable LineWithBold()
+ {
+ yield return new TestCaseData("__word bubo__ bibo", "word bubo bibo", new List()
+ {
+ new BoldTag(0, false),
+ new BoldTag(9, true),
+ });
+ yield return new TestCaseData("__word bu__bo bibo", "__word bu__bo bibo", new List()
+ {
+ });
+ yield return new TestCaseData("wo__rd bubo__ bibo", "wo__rd bubo__ bibo", new List()
+ {
+ });
+ yield return new TestCaseData("wo__rd bu__bo bibo", "wo__rd bu__bo bibo", new List()
+ {
+ });
+ yield return new TestCaseData("__wo__rd bubo bibo", "word bubo bibo", new List()
+ {
+ new BoldTag(0, false),
+ new BoldTag(2, true),
+ });
+ yield return new TestCaseData("__word _bubo_ bibo__", "word bubo bibo", new List()
+ {
+ new BoldTag(0, false),
+ new ItalicTag(5, false),
+ new ItalicTag(9, true),
+ new BoldTag(14, true)
+ });
+ yield return new TestCaseData("l__ov__e", "love", new List()
+ {
+ new BoldTag(1, false),
+ new BoldTag(3, true),
+ });
+ yield return new TestCaseData("l__ove__", "love", new List()
+ {
+ new BoldTag(1, false),
+ new BoldTag(4, true),
+ });
+ yield return new TestCaseData("_word __bubo_ bibo__", "_word __bubo_ bibo__", new List());
+ yield return new TestCaseData(@"\__word bubo__ bibo", "__word bubo__ bibo", new List());
+ yield return new TestCaseData(@"__word bubo\__ bibo", "__word bubo__ bibo", new List());
+ yield return new TestCaseData(@"\__word bubo\__ bibo", @"__word bubo__ bibo", new List());
+ yield return new TestCaseData("Why__1__ word 23 bubo bibo", "Why__1__ word 23 bubo bibo", new List());
+ }
+
+ public static IEnumerable MultiTagsLine()
+ {
+ //Это можно отнести к пересечению тегов
+ yield return new TestCaseData("__word _bubo___", "__word _bubo___", new List()
+ {
+ });
+ //Это тоже по сути пересечение тегов
+ yield return new TestCaseData("___word bubo___", "___word bubo___", new List()
+ {
+ });
+ yield return new TestCaseData("__word _bubo_ love__", "word bubo love", new List()
+ {
+ new BoldTag(0, false),
+ new ItalicTag(5, false),
+ new ItalicTag(9, true),
+ new BoldTag(14, true),
+ });
+ yield return new TestCaseData("___word_ bubo__", "word bubo", new List()
+ {
+ new BoldTag(0, false),
+ new ItalicTag(0, false),
+ new ItalicTag(4, true),
+ new BoldTag(9, true),
+ });
+ yield return new TestCaseData("__word _bu_ bo__", "word bu bo", new List()
+ {
+ new BoldTag(0, false),
+ new ItalicTag(5, false),
+ new ItalicTag(7, true),
+ new BoldTag(10, true),
+ });
+ yield return new TestCaseData("_word __bu__ bo_", "word bu bo", new List()
+ {
+ new ItalicTag(0, false),
+ new BoldTag(5, false),
+ new BoldTag(7, true),
+ new ItalicTag(10, true),
+ });
+ }
+ }
+}
diff --git a/cs/Markdown_Tests/TestData/TokenGeneratorTestsData.cs b/cs/Markdown_Tests/TestData/TokenGeneratorTestsData.cs
new file mode 100644
index 000000000..d4e88a806
--- /dev/null
+++ b/cs/Markdown_Tests/TestData/TokenGeneratorTestsData.cs
@@ -0,0 +1,189 @@
+using Markdown.Tags;
+using Markdown.Tokens;
+
+namespace MarkdownTests.TestData;
+
+public static class TokenGeneratorTestsData
+{
+ public static IEnumerable TextOnlyLines()
+ {
+ yield return new TestCaseData("вася", new List
+ {
+ new(TokenType.Text, "вася", 0)
+ });
+
+ yield return new TestCaseData("петя1223d", new List
+ {
+ new(TokenType.Text, "петя", 0),
+ new(TokenType.Number, "1223", 4),
+ new(TokenType.Text, "d",8)
+ });
+
+ yield return new TestCaseData("1234566", new List
+ {
+ new(TokenType.Number, "1234566", 0)
+ });
+ }
+
+ public static IEnumerable WhiteSpacesOnlyLines()
+ {
+ yield return new TestCaseData(" ", new List
+ {
+ new(TokenType.WhiteSpace, " ", 0)
+ });
+
+ yield return new TestCaseData(" ", new List
+ {
+ new(TokenType.WhiteSpace, " ", 0),
+ new(TokenType.WhiteSpace, " ", 1)
+ });
+
+ yield return new TestCaseData(" ", new List
+ {
+ new(TokenType.WhiteSpace, " ", 0),
+ new(TokenType.WhiteSpace, " ", 1),
+ new(TokenType.WhiteSpace, " ", 2),
+ new(TokenType.WhiteSpace, " ", 3),
+ new(TokenType.WhiteSpace, " ", 4),
+ new(TokenType.WhiteSpace, " ", 5)
+ });
+ }
+
+ public static IEnumerable LinesWithHeader()
+ {
+ yield return new TestCaseData("# Заголовок", new List
+ {
+ new(TokenType.MdTag, "# ", 0, false,TagType.Header),
+ new(TokenType.Text, "Заголовок", 2)
+ });
+
+ yield return new TestCaseData("# # # ", new List
+ {
+ new(TokenType.MdTag, "# ", 0, false, TagType.Header),
+ new(TokenType.MdTag, "# ", 2, false, TagType.Header),
+ new(TokenType.MdTag, "# ", 4, false, TagType.Header)
+ });
+
+ yield return new TestCaseData(@" # # ", new List
+ {
+ new(TokenType.WhiteSpace, " ", 0),
+ new(TokenType.MdTag, "# ", 1, false, TagType.Header),
+ new(TokenType.MdTag, "# ", 3, false, TagType.Header)
+ });
+ }
+
+ public static IEnumerable LinesWithBulletedList()
+ {
+ yield return new TestCaseData("* Заголовок", new List
+ {
+ new(TokenType.MdTag, "* ", 0, false, TagType.BulletedListItem),
+ new(TokenType.Text, "Заголовок", 2)
+ });
+
+ yield return new TestCaseData("* * * ", new List
+ {
+ new(TokenType.MdTag, "* ", 0, false, TagType.BulletedListItem),
+ new(TokenType.MdTag, "* ", 2, false, TagType.BulletedListItem),
+ new(TokenType.MdTag, "* ", 4, false, TagType.BulletedListItem)
+ });
+
+ yield return new TestCaseData(@" * * ", new List
+ {
+ new(TokenType.WhiteSpace, " ", 0),
+ new(TokenType.MdTag, "* ", 1, false, TagType.BulletedListItem),
+ new(TokenType.MdTag, "* ", 3, false, TagType.BulletedListItem)
+ });
+
+ yield return new TestCaseData(@"* раз * два", new List
+ {
+ new(TokenType.MdTag, "* ", 0, false, TagType.BulletedListItem),
+ new(TokenType.Text, "раз", 2),
+ new(TokenType.WhiteSpace, " ", 5),
+ new(TokenType.MdTag, "* ", 6, false, TagType.BulletedListItem),
+ new(TokenType.Text, "два", 8)
+ });
+ }
+
+ public static IEnumerable LinesWithEscapes()
+ {
+ yield return new TestCaseData(@"\", new List
+ {
+ new(TokenType.Escape, @"\", 0)
+ });
+
+ yield return new TestCaseData(@"\\", new List
+ {
+ new(TokenType.Escape, @"\", 0),
+ new(TokenType.Escape, @"\", 1)
+ });
+
+ yield return new TestCaseData(@"\\\", new List
+ {
+ new(TokenType.Escape, @"\", 0),
+ new(TokenType.Escape, @"\", 1),
+ new(TokenType.Escape, @"\", 2),
+ });
+ }
+
+ public static IEnumerable LinesWithUnderscores()
+ {
+ yield return new TestCaseData("_", new List
+ {
+ new(TokenType.MdTag, "_", 0, false, TagType.Italic)
+ });
+
+ yield return new TestCaseData("__", new List
+ {
+ new(TokenType.MdTag, "__", 0, false, TagType.Bold)
+ });
+
+ yield return new TestCaseData("___", new List
+ {
+ new(TokenType.MdTag, "__", 0, false, TagType.Bold),
+ new(TokenType.MdTag, "_", 2, false, TagType.Italic)
+ });
+
+ yield return new TestCaseData("____", new List
+ {
+ new(TokenType.MdTag, "__", 0, false, TagType.Bold),
+ new(TokenType.MdTag, "__", 2, false, TagType.Bold)
+ });
+ }
+
+ public static IEnumerable LineWithMultiTokens()
+ {
+ yield return new TestCaseData("Bibo 234 _ # ", new List
+ {
+ new(TokenType.Text, "Bibo", 0),
+ new(TokenType.WhiteSpace, " ", 4),
+ new(TokenType.Number, "234", 5),
+ new(TokenType.WhiteSpace, " ", 8),
+ new(TokenType.MdTag, "_", 9, false, TagType.Italic),
+ new(TokenType.WhiteSpace, " ", 10),
+ new(TokenType.MdTag, "# ", 11, false, TagType.Header)
+ });
+
+ yield return new TestCaseData("__# _", new List
+ {
+ new(TokenType.MdTag, "__", 0, false, TagType.Bold),
+ new(TokenType.MdTag, "# ", 2, false, TagType.Header),
+ new(TokenType.MdTag, "_", 4, false, TagType.Italic)
+ });
+
+ yield return new TestCaseData("_2_3_", new List
+ {
+ new(TokenType.MdTag, "_", 0, false, TagType.Italic),
+ new(TokenType.Number, "2", 1),
+ new(TokenType.MdTag, "_", 2, false, TagType.Italic),
+ new(TokenType.Number, "3",3),
+ new(TokenType.MdTag, "_", 4, false, TagType.Italic)
+ });
+
+ yield return new TestCaseData(@"\# word", new List
+ {
+ new(TokenType.Escape, @"\", 0),
+ new(TokenType.MdTag, "# ", 1, false, TagType.Header),
+ new(TokenType.Text, "word", 3)
+ });
+ }
+}
\ No newline at end of file
diff --git a/cs/Markdown_Tests/TokenGenerator_Tests.cs b/cs/Markdown_Tests/TokenGenerator_Tests.cs
new file mode 100644
index 000000000..9f7de4af0
--- /dev/null
+++ b/cs/Markdown_Tests/TokenGenerator_Tests.cs
@@ -0,0 +1,96 @@
+using FluentAssertions;
+using Markdown.Tags;
+using Markdown.TokenGeneratorClasses;
+using Markdown.Tokens;
+using MarkdownTests.TestData;
+
+namespace MarkdownTests;
+
+public class TokenGenerator_Tests
+{
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.TextOnlyLines))]
+ public void TokenGenerator_GetTokenCorrectly_WhenTextOnly(string input, List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.WhiteSpacesOnlyLines))]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenWhiteSpacesOnly(string input, List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+ [Test]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenHeaderTokenOnly()
+ {
+ var actuallyTokens = GetAllTokensFromLine("# ");
+
+ actuallyTokens.Should().BeEquivalentTo(new List
+ {
+ new(TokenType.MdTag, "# ", 0, false, TagType.Header)
+ });
+ }
+
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.LinesWithHeader))]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenLineWithHeaders(string input, List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.LinesWithBulletedList))]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenLineWithBulletedLis(string input,
+ List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.LinesWithEscapes))]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenLineWithEscapes(string input, List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.LinesWithUnderscores))]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenLineWithUnderscores(string input,
+ List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+ [TestCaseSource(typeof(TokenGeneratorTestsData), nameof(TokenGeneratorTestsData.LineWithMultiTokens))]
+ public void TokenGenerator_GetTokensBySymbolCorrectly_WhenLineWithMultiTokens(string input,
+ List expectedTokens)
+ {
+ var actuallyTokens = GetAllTokensFromLine(input);
+
+ actuallyTokens.Should().BeEquivalentTo(expectedTokens);
+ }
+
+
+ private static List GetAllTokensFromLine(string line)
+ {
+ var i = 0;
+ var result = new List();
+
+ while (i < line.Length)
+ {
+ var token = TokenGenerator.GetToken(line, i);
+ result.Add(token);
+ i += token.Content.Length;
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/cs/clean-code.sln b/cs/clean-code.sln
index 2206d54db..0cb047aea 100644
--- a/cs/clean-code.sln
+++ b/cs/clean-code.sln
@@ -1,13 +1,17 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chess", "Chess\Chess.csproj", "{DBFBE40E-EE0C-48F4-8763-EBD11C960081}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chess", "Chess\Chess.csproj", "{DBFBE40E-EE0C-48F4-8763-EBD11C960081}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigit\ControlDigit.csproj", "{B06A4B35-9D61-4A63-9167-0673F20CA989}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlDigit", "ControlDigit\ControlDigit.csproj", "{B06A4B35-9D61-4A63-9167-0673F20CA989}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{3C26D892-E2EE-47D5-811C-1915F72BEA50}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownTests", "Markdown_Tests\MarkdownTests.csproj", "{7D3EFA76-EEF6-49DF-BF0D-6688D14E1341}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,5 +31,19 @@ Global
{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C26D892-E2EE-47D5-811C-1915F72BEA50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C26D892-E2EE-47D5-811C-1915F72BEA50}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C26D892-E2EE-47D5-811C-1915F72BEA50}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C26D892-E2EE-47D5-811C-1915F72BEA50}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D3EFA76-EEF6-49DF-BF0D-6688D14E1341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D3EFA76-EEF6-49DF-BF0D-6688D14E1341}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D3EFA76-EEF6-49DF-BF0D-6688D14E1341}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D3EFA76-EEF6-49DF-BF0D-6688D14E1341}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4980313C-A4CA-4D98-A626-19568120E067}
EndGlobalSection
EndGlobal
diff --git a/cs/clean-code.sln.DotSettings b/cs/clean-code.sln.DotSettings
index 135b83ecb..229f449d2 100644
--- a/cs/clean-code.sln.DotSettings
+++ b/cs/clean-code.sln.DotSettings
@@ -1,6 +1,9 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy>
+ True
True
True
Imported 10.10.2016