diff --git a/cs/Markdown/Extantions.cs b/cs/Markdown/Extantions.cs new file mode 100644 index 000000000..8b30da692 --- /dev/null +++ b/cs/Markdown/Extantions.cs @@ -0,0 +1,36 @@ +using Markdown.Tokens; +using Markdown.Tokens.HtmlToken; + +namespace Markdown; + +public static class Extantions +{ + public static bool IsOnlyDight(this IEnumerable tokens) + { + if (tokens.Count() < 3) + throw new ArgumentException("Method takes at least three node"); + return tokens + .Skip(1) + .Take(tokens.Count() - 2) + .SelectMany(node => node.Value) + .All(char.IsDigit); + } + public static IEnumerable? TextifyTags(this IEnumerable nodes) + { + foreach (var node in nodes) + { + if (node is TextToken or HtmlTokenWithTag) + yield return node; + else + yield return new TextToken(node.Value); + } + } + public static IEnumerable InnerElements(this IEnumerable enumerable) + { + if (enumerable.Count() < 2) + throw new ArgumentException("Should have at least 2 elements"); + if (enumerable.Count() == 2) + return Enumerable.Empty(); + return enumerable.Skip(1).Take(enumerable.Count() - 2); + } +} \ No newline at end of file diff --git a/cs/Markdown/ITranslationResult.cs b/cs/Markdown/ITranslationResult.cs new file mode 100644 index 000000000..f29ae15bb --- /dev/null +++ b/cs/Markdown/ITranslationResult.cs @@ -0,0 +1,6 @@ +namespace Markdown; + +public interface ITranslationResult +{ + +} \ No newline at end of file diff --git a/cs/Markdown/Maker/HtmlMaker.cs b/cs/Markdown/Maker/HtmlMaker.cs new file mode 100644 index 000000000..de3ee0ac8 --- /dev/null +++ b/cs/Markdown/Maker/HtmlMaker.cs @@ -0,0 +1,244 @@ +using System.Net.NetworkInformation; +using Markdown.Tokens; +using Markdown.Tokens.HtmlToken; +using Markdown.Tokens.HtmlToken.Header; +using Markdown.Tokens.HtmlToken.Italic; +using Markdown.Tokens.HtmlToken.ListItem; +using Markdown.Tokens.HtmlToken.Strong; +using Markdown.Tokens.HtmlToken.UnorderedList; +using Markdown.Tokens.StringToken; + +namespace Markdown.Maker; + +public class HtmlMaker : IMaker +{ + private Stack rootChildren = new Stack(); + private Stack<(int pos, BaseHtmlToken token)> temporaryStack = new Stack<(int, BaseHtmlToken)>(); + private StringToken[] tokens; + private int position; + + + private StringToken Peek(int offset) + { + var index = position + offset; + if (index >= tokens.Length) + return tokens[^1]; + return index < 0 ? tokens[0] : tokens[index]; + } + + private StringToken Next => Peek(1); + private StringToken Current => Peek(0); + private StringToken Previous => Peek(-1); + + public RootToken MakeFromTokens(IEnumerable input) + { + tokens = input.ToArray(); + List? children; + while (position < tokens.Length) + { + switch (Current.Type) + { + case StringTokenType.Unexpected: + case StringTokenType.Text: + rootChildren.Push(new TextToken(Current.Value)); + break; + case StringTokenType.WhiteSpace: + var unclosedTags = new Stack<(int, BaseHtmlToken)>(); + while (temporaryStack.Count > 0) + { + var (nodeTokenIndex, node) = temporaryStack.Pop(); + switch (node) + { + case ItalicOpenToken: + case StrongOpenToken: + if ((nodeTokenIndex == 0 || + tokens[nodeTokenIndex - 1].Type == StringTokenType.WhiteSpace) && + tokens[nodeTokenIndex + 1].Type != StringTokenType.WhiteSpace) + unclosedTags.Push((nodeTokenIndex, node)); + break; + default: + unclosedTags.Push((nodeTokenIndex, node)); + break; + } + } + + foreach (var tag in unclosedTags) + temporaryStack.Push(tag); + rootChildren.Push(new TextToken(Current.Value)); + break; + case StringTokenType.NewLine: + if (temporaryStack.Any(pair => pair.token is HeaderOpenToken)) + { + temporaryStack.Clear(); + rootChildren.Push(new HeaderCloseToken(Current.Value)); + children = UnitTokenEnds().ToList(); + rootChildren.Push(new HeaderToken(children)); + rootChildren.Push(new TextToken(Current.Value)); + break; + } + + if (temporaryStack.Any(pair => pair.token is ListItemOpenToken)) + { + temporaryStack.Clear(); + rootChildren.Push(new ListItemCloseToken(Current.Value)); + children = UnitTokenEnds().ToList(); + rootChildren.Push(new ListItemToken(children)); + rootChildren.Push(new TextToken(Current.Value)); + break; + } + + rootChildren.Push(new TextToken(Current.Value)); + break; + + case StringTokenType.Dash: + if (Next.Type == StringTokenType.WhiteSpace && + (Previous.Type == StringTokenType.NewLine || position == 0)) + { + if (TryOpenBodyWith(new ListItemOpenToken(Current.Value + Next.Value))) + { + position++; + break; + } + } + + rootChildren.Push(new TextToken(Current.Value)); + + break; + + case StringTokenType.Hash: + if (Next.Type != StringTokenType.WhiteSpace || + (Previous.Type != StringTokenType.NewLine && position != 0)) + { + rootChildren.Push(new TextToken(Current.Value)); + break; + } + + if (TryOpenBodyWith(new HeaderOpenToken(Current.Value + Next.Value))) + { + position++; + break; + } + + rootChildren.Push(new TextToken(Current.Value)); + break; + case StringTokenType.SingleUnderscore: + if (TryOpenBodyWith(new ItalicOpenToken(Current.Value))) + break; + + rootChildren.Push(new ItalicCloseToken(Current.Value)); + children = UnitTokenEnds() + .Select(token => token is StrongToken ? new TextToken(token.Value) : token).ToList(); + temporaryStack.Pop(); + + if (children.IsOnlyDight()) + rootChildren.Push(new TextToken(string.Join("", children.Select(child => child.Value)))); + else + rootChildren.Push(new ItalicToken(children)); + break; + case StringTokenType.DoubleUnderscore: + if (TryOpenBodyWith(new StrongOpenToken(Current.Value))) + break; + + rootChildren.Push(new StrongCloseToken(Current.Value)); + children = UnitTokenEnds().ToList(); + temporaryStack.Pop(); + + if (children.Count == 2) + rootChildren.Push(new TextToken(children[0].Value + children[1].Value)); + else if (children.IsOnlyDight()) + rootChildren.Push(new TextToken(string.Join("", children.Select(child => child.Value)))); + else + rootChildren.Push(new StrongToken(children)); + + break; + default: + throw new Exception("Unimplemented token"); + } + + position++; + } + + if (temporaryStack.Any(pair => pair.token is HeaderOpenToken)) + { + children = new List() { new HeaderCloseToken("") }; + while (true) + { + var child = rootChildren.Pop(); + children.Add(child); + if (child is HeaderOpenToken) + break; + } + + children.Reverse(); + + rootChildren.Push(new HeaderToken(children)); + } + + if (temporaryStack.Any(pair => pair.token is ListItemOpenToken)) + { + children = new List() { new ListItemCloseToken("") }; + while (true) + { + var child = rootChildren.Pop(); + children.Add(child); + if (child is ListItemOpenToken) + break; + } + + children.Reverse(); + + rootChildren.Push(new HeaderToken(children)); + } + + + return new RootToken(rootChildren.Reverse().TextifyTags().ToList()); + } + + private bool TryOpenBodyWith(BaseHtmlToken token) + { + //проврка на закрытие + if (!temporaryStack.Any(pair => pair.token.GetType() == token.GetType())) + { + rootChildren.Push(token); + temporaryStack.Push((position, token)); + return true; + } + + //проверка на неверную вложенность + if (temporaryStack.Peek().token.GetType() != token.GetType()) + { + var last = temporaryStack.Pop().token; + rootChildren.Push(new TextToken(Current.Value)); + while (last.GetType() != token.GetType()) + last = temporaryStack.Pop().token; + return true; + } + + + if (Previous.Type == StringTokenType.WhiteSpace) + { + rootChildren.Push(new TextToken(token.Value)); + return true; + } + + return false; + } + + private IEnumerable UnitTokenEnds() where TOpenToken : BaseHtmlToken + { + var list = new List(); + while (true) + { + var child = rootChildren.Pop(); + list.Add(child); + if (child is TOpenToken) + break; + } + + list.Reverse(); + yield return list.First(); + foreach (var node in list.Skip(1).Take(list.Count() - 2).TextifyTags()) + yield return node; + yield return list.Last(); + } +} \ No newline at end of file diff --git a/cs/Markdown/Maker/IMaker.cs b/cs/Markdown/Maker/IMaker.cs new file mode 100644 index 000000000..3d6e4a1b9 --- /dev/null +++ b/cs/Markdown/Maker/IMaker.cs @@ -0,0 +1,8 @@ +using Markdown.Tokens.StringToken; + +namespace Markdown.Maker; + +public interface IMaker +{ + public T MakeFromTokens(IEnumerable tokens); +} \ No newline at end of file diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..2f4fc7765 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..96dc7483a --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,19 @@ +using Markdown.Maker; +using Markdown.Parser; +using Markdown.Rendered; +using Markdown.Tokens; +using Markdown.Tokens.HtmlToken; + +namespace Markdown; + +public class Md(IParser parser, IMaker maker, IRenderer renderer) +{ + + + public string Render(string input) + { + var tokens = parser.Parse(input); + var model = maker.MakeFromTokens(tokens); + return renderer.Render(model); + } +} \ No newline at end of file diff --git a/cs/Markdown/Parser/IParser.cs b/cs/Markdown/Parser/IParser.cs new file mode 100644 index 000000000..a2ebe2e53 --- /dev/null +++ b/cs/Markdown/Parser/IParser.cs @@ -0,0 +1,8 @@ +using Markdown.Tokens.StringToken; + +namespace Markdown.Parser; + +public interface IParser +{ + public IEnumerable Parse(string input); +} \ No newline at end of file diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs new file mode 100644 index 000000000..1ac7e72ca --- /dev/null +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -0,0 +1,90 @@ +using Markdown.Parser; +using Markdown.Tokens.StringToken; + +namespace Markdown; + +public class MarkdownParser() : IParser +{ + private int position; + private string text; + private char Peek(int offset) + { + var index = position + offset; + return index >= text.Length ? '\0' : text[index]; + } + + private char Current => Peek(0); + private char Next => Peek(1); + private static bool IsMarkupSymbol(char c) => @"_#\".Contains(c); + + public IEnumerable Parse(string text) + { + this.text = text; + while (Current != '\0'){ + switch (Current) + { + case '\n': + case '\r': + yield return new StringToken(Current.ToString(), position, StringTokenType.NewLine); + break; + case ' ': + case '\t': + { + var start = position; + while (Next == ' ' || Next == '\t') + position++; + + var length = position - start + 1; + var tokenText = text.Substring(start, length); + yield return new StringToken(tokenText, start, StringTokenType.WhiteSpace); + break; + } + case '#': + yield return new StringToken("#", position, StringTokenType.Hash); + break; + case '_': + { + var start = position; + if (Next == '_') + { + position++; + yield return new StringToken("__", start, StringTokenType.DoubleUnderscore); + } + else + yield return new StringToken("_", start, StringTokenType.SingleUnderscore); + + break; + } + case '-': + { + yield return new StringToken("-", position, StringTokenType.Dash); + break; + } + default: + { + if (!char.IsLetter(Current) && Current != '\\') + { + yield return new StringToken(Current.ToString(), position, StringTokenType.Unexpected); + break; + } + + var start = position; + var letters = new List(); + while (Current != '\0' && (char.IsLetter(Current) || Current == '\\')) + { + if (Current == '\\' && IsMarkupSymbol(Next)) + position++; + + letters.Add(Current); + position++; + } + + yield return new StringToken(new string(letters.ToArray()), start, StringTokenType.Text); + continue; + } + } + + position++; + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Program.cs b/cs/Markdown/Program.cs new file mode 100644 index 000000000..e5dff12bc --- /dev/null +++ b/cs/Markdown/Program.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +Console.WriteLine("Hello, World!"); \ No newline at end of file diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs new file mode 100644 index 000000000..58f4c6125 --- /dev/null +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -0,0 +1,13 @@ +using Markdown.Rendered; +using Markdown.Tokens.HtmlToken; +using Markdown.Tokens.StringToken; + +namespace Markdown; + +public class HtmlRenderer : IRenderer +{ + public string Render(RootToken root) + { + return root.ToString() ?? string.Empty; + } +} \ No newline at end of file diff --git a/cs/Markdown/Renderer/IRednderer.cs b/cs/Markdown/Renderer/IRednderer.cs new file mode 100644 index 000000000..d3d33f87f --- /dev/null +++ b/cs/Markdown/Renderer/IRednderer.cs @@ -0,0 +1,6 @@ +namespace Markdown.Rendered; + +public interface IRenderer +{ + public string Render(T input); +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/BaseHtmlToken.cs b/cs/Markdown/Tokens/HtmlToken/BaseHtmlToken.cs new file mode 100644 index 000000000..6653f42b1 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/BaseHtmlToken.cs @@ -0,0 +1,12 @@ +namespace Markdown.Tokens.HtmlToken; + +public abstract class BaseHtmlToken +{ + public List Children { get; } + public abstract string Value { get; } + + public BaseHtmlToken(List? children) + { + Children = children; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Header/HeaderCloseToken.cs b/cs/Markdown/Tokens/HtmlToken/Header/HeaderCloseToken.cs new file mode 100644 index 000000000..0dbd707fe --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Header/HeaderCloseToken.cs @@ -0,0 +1,8 @@ +namespace Markdown.Tokens.HtmlToken.Header; + +public class HeaderCloseToken : SingleToken +{ + public HeaderCloseToken(string value) : base(value) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Header/HeaderOpenToken.cs b/cs/Markdown/Tokens/HtmlToken/Header/HeaderOpenToken.cs new file mode 100644 index 000000000..31d6dbe3c --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Header/HeaderOpenToken.cs @@ -0,0 +1,8 @@ +namespace Markdown.Tokens.HtmlToken.Header; + +public class HeaderOpenToken : SingleToken +{ + public HeaderOpenToken(string value) : base(value) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Header/HeaderToken.cs b/cs/Markdown/Tokens/HtmlToken/Header/HeaderToken.cs new file mode 100644 index 000000000..85000a004 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Header/HeaderToken.cs @@ -0,0 +1,12 @@ +namespace Markdown.Tokens.HtmlToken.Header; + +public class HeaderToken : HtmlTokenWithTag +{ + public HeaderToken(List? children) : base(children) + { + } + + public override Type OpenTypeToken => typeof(HeaderOpenToken); + public override Type CloseTypeToken => typeof(HeaderCloseToken); + public override string TagValue => "h1"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/HtmlTokenWithTag.cs b/cs/Markdown/Tokens/HtmlToken/HtmlTokenWithTag.cs new file mode 100644 index 000000000..4d6162060 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/HtmlTokenWithTag.cs @@ -0,0 +1,18 @@ +namespace Markdown.Tokens.HtmlToken; + +public abstract class HtmlTokenWithTag : BaseHtmlToken +{ + public HtmlTokenWithTag(List? children) : base(children) + { + //проверка на валидность по типам + } + + public abstract Type OpenTypeToken { get; } + public abstract Type CloseTypeToken { get; } + public abstract string TagValue { get; } + public override string Value => string.Join("", Children!.Select(child => child.Value)); + public override string ToString() + { + return $"<{TagValue}>{string.Join("", Children.InnerElements().Select(token => token.ToString()))}"; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Italic/ItalicCloseToken.cs b/cs/Markdown/Tokens/HtmlToken/Italic/ItalicCloseToken.cs new file mode 100644 index 000000000..4c9d63f47 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Italic/ItalicCloseToken.cs @@ -0,0 +1,8 @@ +namespace Markdown.Tokens.HtmlToken.Italic; + +public class ItalicCloseToken : SingleToken +{ + public ItalicCloseToken(string value) : base(value) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Italic/ItalicOpenToken.cs b/cs/Markdown/Tokens/HtmlToken/Italic/ItalicOpenToken.cs new file mode 100644 index 000000000..a41306c3d --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Italic/ItalicOpenToken.cs @@ -0,0 +1,8 @@ +namespace Markdown.Tokens.HtmlToken.Italic; + +public class ItalicOpenToken : SingleToken +{ + public ItalicOpenToken(string value) : base(value) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Italic/ItalicToken.cs b/cs/Markdown/Tokens/HtmlToken/Italic/ItalicToken.cs new file mode 100644 index 000000000..36974a198 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Italic/ItalicToken.cs @@ -0,0 +1,11 @@ +namespace Markdown.Tokens.HtmlToken.Italic; + +public class ItalicToken : HtmlTokenWithTag +{ + public ItalicToken(List? children) : base(children) + { + } + public override Type OpenTypeToken => typeof(ItalicOpenToken); + public override Type CloseTypeToken => typeof(ItalicCloseToken); + public override string TagValue => "em"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemCloseToken.cs b/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemCloseToken.cs new file mode 100644 index 000000000..0c8db3340 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemCloseToken.cs @@ -0,0 +1,12 @@ +namespace Markdown.Tokens.HtmlToken.ListItem; + +public class ListItemCloseToken : HtmlTokenWithTag +{ + public ListItemCloseToken(string value) : base(null) + { + } + + public override Type OpenTypeToken => typeof(ListItemOpenToken); + public override Type CloseTypeToken => typeof(ListItemCloseToken); + public override string TagValue => "li"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemOpenToken.cs b/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemOpenToken.cs new file mode 100644 index 000000000..8c6c9f85b --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemOpenToken.cs @@ -0,0 +1,12 @@ +namespace Markdown.Tokens.HtmlToken.ListItem; + +public class ListItemOpenToken : HtmlTokenWithTag +{ + public ListItemOpenToken(string value) : base(null) + { + } + + public override Type OpenTypeToken => typeof(ListItemOpenToken); + public override Type CloseTypeToken => typeof(ListItemCloseToken); + public override string TagValue => "li"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemToken.cs b/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemToken.cs new file mode 100644 index 000000000..05f8e661c --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/ListItem/ListItemToken.cs @@ -0,0 +1,13 @@ +namespace Markdown.Tokens.HtmlToken.ListItem; + +public class ListItemToken : HtmlTokenWithTag +{ + public ListItemToken(List? children) : base(children) + { + + } + + public override Type OpenTypeToken => typeof(ListItemOpenToken); + public override Type CloseTypeToken => typeof(ListItemCloseToken); + public override string TagValue => "li"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/RootToken.cs b/cs/Markdown/Tokens/HtmlToken/RootToken.cs new file mode 100644 index 000000000..2cc6783ae --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/RootToken.cs @@ -0,0 +1,11 @@ +namespace Markdown.Tokens.HtmlToken; + +public class RootToken : BaseHtmlToken, ITranslationResult +{ + public RootToken(List? children) : base(children) + { + } + + public override string ToString() => string.Join("", Children!.Select(child => child.ToString())); + public override string Value => string.Join("", Children!); +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/SingleToken.cs b/cs/Markdown/Tokens/HtmlToken/SingleToken.cs new file mode 100644 index 000000000..4737003d8 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/SingleToken.cs @@ -0,0 +1,12 @@ +using Markdown.Tokens.HtmlToken; + +namespace Markdown.Tokens; + +public class SingleToken : BaseHtmlToken +{ + public SingleToken(string value) : base(null) + { + Value = value; + } + public override string Value { get; } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Strong/StrongCloseToken.cs b/cs/Markdown/Tokens/HtmlToken/Strong/StrongCloseToken.cs new file mode 100644 index 000000000..06bb4c070 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Strong/StrongCloseToken.cs @@ -0,0 +1,9 @@ +namespace Markdown.Tokens.HtmlToken.Strong; + +public class StrongCloseToken : SingleToken +{ + public StrongCloseToken(string value) : base(value) + { + + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Strong/StrongOpenToken.cs b/cs/Markdown/Tokens/HtmlToken/Strong/StrongOpenToken.cs new file mode 100644 index 000000000..071c4f539 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Strong/StrongOpenToken.cs @@ -0,0 +1,9 @@ +namespace Markdown.Tokens.HtmlToken.Strong; + +public class StrongOpenToken : SingleToken +{ + public StrongOpenToken(string value) : base(value) + { + } + +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/Strong/StrongToken.cs b/cs/Markdown/Tokens/HtmlToken/Strong/StrongToken.cs new file mode 100644 index 000000000..eab67a6be --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/Strong/StrongToken.cs @@ -0,0 +1,12 @@ +namespace Markdown.Tokens.HtmlToken.Strong; + +public class StrongToken : HtmlTokenWithTag +{ + public StrongToken(List? children) : base(children) + { + } + + public override Type OpenTypeToken => typeof(StrongOpenToken); + public override Type CloseTypeToken => typeof(StrongCloseToken); + public override string TagValue => "strong"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/TextToken.cs b/cs/Markdown/Tokens/HtmlToken/TextToken.cs new file mode 100644 index 000000000..17b02e9c0 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/TextToken.cs @@ -0,0 +1,13 @@ +namespace Markdown.Tokens.HtmlToken; + +public class TextToken : SingleToken +{ + public TextToken(string value) : base(value) + { + } + + public override string ToString() + { + return Value; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListCloseToken.cs b/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListCloseToken.cs new file mode 100644 index 000000000..7823a65a8 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListCloseToken.cs @@ -0,0 +1,8 @@ +namespace Markdown.Tokens.HtmlToken.UnorderedList; + +public class UnorderedListCloseToken : SingleToken +{ + public UnorderedListCloseToken(string value) : base(value) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListOpenToken.cs b/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListOpenToken.cs new file mode 100644 index 000000000..21818f168 --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListOpenToken.cs @@ -0,0 +1,8 @@ +namespace Markdown.Tokens.HtmlToken.UnorderedList; + +public class UnorderedListOpenToken : SingleToken +{ + public UnorderedListOpenToken(string value) : base(value) + { + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListToken.cs b/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListToken.cs new file mode 100644 index 000000000..339677e8b --- /dev/null +++ b/cs/Markdown/Tokens/HtmlToken/UnorderedList/UnorderedListToken.cs @@ -0,0 +1,11 @@ +namespace Markdown.Tokens.HtmlToken.UnorderedList; + +public class UnorderedListToken : HtmlTokenWithTag +{ + public UnorderedListToken(List? children) : base(children) + { + } + public override Type OpenTypeToken => typeof(UnorderedListOpenToken); + public override Type CloseTypeToken => typeof(UnorderedListCloseToken); + public override string TagValue => "ul"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/StringToken/StringToken.cs b/cs/Markdown/Tokens/StringToken/StringToken.cs new file mode 100644 index 000000000..8eeff060d --- /dev/null +++ b/cs/Markdown/Tokens/StringToken/StringToken.cs @@ -0,0 +1,9 @@ +namespace Markdown.Tokens.StringToken; + +public class StringToken(string value, int offset, StringTokenType type) +{ + public int Length = value.Length; + public int Offset = offset; + public string Value = value; + public StringTokenType Type = type; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/StringToken/StringTokenType.cs b/cs/Markdown/Tokens/StringToken/StringTokenType.cs new file mode 100644 index 000000000..35d8e7240 --- /dev/null +++ b/cs/Markdown/Tokens/StringToken/StringTokenType.cs @@ -0,0 +1,13 @@ +namespace Markdown.Tokens.StringToken; + +public enum StringTokenType +{ + Text, + NewLine, + WhiteSpace, + Hash, + Unexpected, + DoubleUnderscore, + SingleUnderscore, + Dash +} \ No newline at end of file diff --git a/cs/MarkdownTest/MarkdownParserTest.cs b/cs/MarkdownTest/MarkdownParserTest.cs new file mode 100644 index 000000000..0155740b7 --- /dev/null +++ b/cs/MarkdownTest/MarkdownParserTest.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using Markdown; +using Markdown.Tokens.StringToken; +using MarkdownTest.TestData; + +namespace MarkdownTest; + +public class LexerTest +{ + [TestCaseSource(typeof(ParserTestData), nameof(ParserTestData.RightKindsData))] + public void ParseRightKindsOrder(string expression, StringTokenType[] kinds) + { + var result = new MarkdownParser() + .Parse(expression) + .Select(tok => tok.Type).ToList(); + result.Should() + .BeEquivalentTo(kinds); + } + + [TestCaseSource(typeof(ParserTestData), nameof(ParserTestData.RightTexts))] + public void ParseWordsFromText(string expression, string[] words) + { + new MarkdownParser() + .Parse(expression) + .Where(tok => tok.Type == StringTokenType.Text) + .Select(tok => tok.Value) + .Should() + .BeEquivalentTo(words); + } +} \ No newline at end of file diff --git a/cs/MarkdownTest/MarkdownTest.csproj b/cs/MarkdownTest/MarkdownTest.csproj new file mode 100644 index 000000000..505542f22 --- /dev/null +++ b/cs/MarkdownTest/MarkdownTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/cs/MarkdownTest/MdTests.cs b/cs/MarkdownTest/MdTests.cs new file mode 100644 index 000000000..11a69137f --- /dev/null +++ b/cs/MarkdownTest/MdTests.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using FluentAssertions; +using Markdown; +using Markdown.Maker; +using MarkdownTest.TestData; + +namespace MarkdownTest; + +public class MdTest +{ + [TestCaseSource(typeof(MdTestData), nameof(MdTestData.SpecExamples))] + public void Test(string md, string html) + { + var result = new Md(new MarkdownParser(), new HtmlMaker(), new HtmlRenderer()) + .Render(md); + result.Should() + .Be(html); + } + + [Test] + public void LinearTimeComlexityTest() + { + var mdExpression = + "#Заголовок с _курсивом_ и __жирным выделением__\n"; + var md = new Md(new MarkdownParser(), new HtmlMaker(), new HtmlRenderer()); + + var stopwatch = new Stopwatch(); + GC.Collect(); + stopwatch.Start(); + md.Render(mdExpression); + stopwatch.Stop(); + GC.Collect(); + + var previous = stopwatch.ElapsedTicks; + long current; + for (int i = 0; i < 10; i++) + { + mdExpression += mdExpression; + stopwatch.Restart(); + md.Render(mdExpression); + stopwatch.Stop(); + GC.Collect(); + current = stopwatch.ElapsedTicks; + Assert.That(current / previous <= 2); + previous = current; + } + } +} \ No newline at end of file diff --git a/cs/MarkdownTest/TestData/MdTestCases.cs b/cs/MarkdownTest/TestData/MdTestCases.cs new file mode 100644 index 000000000..2fb6094df --- /dev/null +++ b/cs/MarkdownTest/TestData/MdTestCases.cs @@ -0,0 +1,79 @@ +namespace MarkdownTest.TestData; + +public class MdTestData +{ + public static TestCaseData[] SpecExamples = + { + new TestCaseData( + "Текст, _окруженный с двух сторон_ одинарными символами подчерка,\nдолжен помещаться в HTML-тег .", + "Текст, окруженный с двух сторон одинарными символами подчерка,\nдолжен помещаться в HTML-тег ." + ).SetName("SimpleEmTagging"), + new TestCaseData( + "__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега .", + "Выделенный двумя символами текст должен становиться полужирным с помощью тега ." + ).SetName("SimpleStrongTagging"), + new TestCaseData( + "Любой символ можно экранировать, чтобы он не считался частью разметки.\n \\_Вот это\\_, не должно выделиться тегом .", + "Любой символ можно экранировать, чтобы он не считался частью разметки.\n _Вот это_, не должно выделиться тегом ." + ).SetName("TagShielding"), + new TestCaseData( + "Символ экранирования исчезает из результата, только если экранирует что-то.\nЗдесь сим\\волы экранирования\\ \\должны остаться.\\", + "Символ экранирования исчезает из результата, только если экранирует что-то.\nЗдесь сим\\волы экранирования\\ \\должны остаться.\\" + ).SetName("ReverseSlashes_RemainItselfIfDoesNotShielding"), + new TestCaseData( + @"Символ экранирования тоже можно экранировать: \\_вот_ это будет выделено тегом ", + @"Символ экранирования тоже можно экранировать: \вот это будет выделено тегом " + ).SetName("ShieldingReverseSlash"), + new TestCaseData( + "Внутри __двойного выделения _одинарное_ тоже__ работает.", + "Внутри двойного выделения одинарное тоже работает." + ).SetName("EmInStrongWork"), + new TestCaseData( + "Но не наоборот — внутри _одинарного __двойное__ не_ работает.", + "Но не наоборот — внутри одинарного __двойное__ не работает." + ).SetName("StrongInEmDoesNotWork"), + new TestCaseData( + "Подчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка.", + "Подчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка." + ).SetName("TaggingNumber_RemainUnderscores"), + new TestCaseData( + "Однако выделять часть слова они могут: и в _нач_але, и в сер_еди_не, и в кон_це._", + "Однако выделять часть слова они могут: и в начале, и в середине, и в конце." + ).SetName("TaggingInDifferentPartsOfWord"), + new TestCaseData( + "В то же время выделение в ра_зных сл_овах не работает.", + "В то же время выделение в ра_зных сл_овах не работает." + ).SetName("TaggingInDifferentWordsDoesNotWork"), + new TestCaseData( + "__Непарные_ символы в рамках одного абзаца не считаются выделением.", + "__Непарные_ символы в рамках одного абзаца не считаются выделением." + ).SetName("NotPairedSymbols_RemainIteself"), + new TestCaseData( + "За подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением \nи остаются просто символами подчерка.", + "За подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением \nи остаются просто символами подчерка." + ).SetName("SingleUnderscoreWithWhitespaceAhead_RemainUnderscore"), + new TestCaseData( + "Подчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки _не считаются_ окончанием выделения \nи остаются просто символами подчерка.", + "Подчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти подчерки _не считаются окончанием выделения \nи остаются просто символами подчерка." + ).SetName("SingleInderscore_RemainUnderscoreInEmTag"), + new TestCaseData( + "В случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением.", + "В случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением." + ).SetName("TagsIntersection_RemainUnderscores"), + new TestCaseData( + "Если внутри подчерков пустая строка ____, то они остаются символами подчерка.", + "Если внутри подчерков пустая строка ____, то они остаются символами подчерка." + ).SetName("TwoDoubleUnderscoresInRow_RemainUnderscores"), + new TestCaseData( + "# Заголовок __с _разными_ символами__", + "

Заголовок с разными символами

" + ).SetName("HeaderWithInnerTags"), + new TestCaseData( + "- First item\n" + + "- Second item\n" + + "- Third item\n", + "
  • First item
  • \n
  • Second item
  • \n
  • Third item
  • \n" + ).SetName("UnorderedList"), + }; + +} \ No newline at end of file diff --git a/cs/MarkdownTest/TestData/ParserTestCases.cs b/cs/MarkdownTest/TestData/ParserTestCases.cs new file mode 100644 index 000000000..4c7f4dae2 --- /dev/null +++ b/cs/MarkdownTest/TestData/ParserTestCases.cs @@ -0,0 +1,103 @@ +using Markdown; +using Markdown.Tokens.StringToken; + +namespace MarkdownTest.TestData; + +public class ParserTestData +{ + public static TestCaseData[] RightTexts = + { + new TestCaseData("a", new[] { "a" }) + .SetName("SingleLetterWords"), + new TestCaseData("abc", new[] { "abc" }) + .SetName("SingleWord"), + new TestCaseData("ab_cd", new[] { "ab", "cd" }) + .SetName("TwoWords_SeparatedBySingleUnderscore"), + new TestCaseData("ab__cd", new[] { "ab", "cd" }) + .SetName("TwoWords_SeparatedByDoubleUnderscore"), + new TestCaseData("ab cd", new[] { "ab", "cd" }) + .SetName("TwoWords_SeparatedByWhitespace"), + new TestCaseData("abc__def_ghi jkl", new[] { "abc", "def", "ghi", "jkl" }) + .SetName("ThreeWords_SeparatedByDifferentTags"), + new TestCaseData(@"\_", new[] { "_" }) + .SetName("ShieldedUnderscore"), + new TestCaseData(@"\__", new[] { "_" }) + .SetName("ShieldedUnderscoreAndSingleUnderscore"), + new TestCaseData(@"\___", new[] { "_" }) + .SetName("ShieldedUnderscoreAndDoubleUnderscore"), + new TestCaseData(@"_\__", new[] { "_" }) + .SetName("ShieldedUnderscoreBetweenSingleUnderscores"), + new TestCaseData(@"\_\_", new[] { "__" }) + .SetName("TwoShieldedUnderscoresInRow"), + }; + + public static TestCaseData[] RightKindsData = + { + new TestCaseData( + "_", + new[] { StringTokenType.SingleUnderscore } + ) + .SetName("OneSingleUnderScore"), + new TestCaseData( + "_ _", + new[] { StringTokenType.SingleUnderscore, StringTokenType.WhiteSpace, StringTokenType.SingleUnderscore } + ) + .SetName("TwoSingleUnderScore_DividedByWhitespace"), + new TestCaseData( + "_ _", + new[] { StringTokenType.SingleUnderscore, StringTokenType.WhiteSpace, StringTokenType.SingleUnderscore } + ) + .SetName("TwoSingleUnderScore_DividedByWhitespaces"), + new TestCaseData( + "_abc_", + new[] { StringTokenType.SingleUnderscore, StringTokenType.Text, StringTokenType.SingleUnderscore } + ) + .SetName("TwoSingleUnderScoreDividedByWord"), + new TestCaseData( + "_a_", + new[] { StringTokenType.SingleUnderscore, StringTokenType.Text, StringTokenType.SingleUnderscore } + ) + .SetName("TwoSingleUnderScoreDividedBySingleLetterWord"), + new TestCaseData( + "__", + new[] { StringTokenType.DoubleUnderscore } + ) + .SetName("OneDoubleUnderscore"), + new TestCaseData( + "____", + new[] { StringTokenType.DoubleUnderscore, StringTokenType.DoubleUnderscore } + ) + .SetName("TwoDoubleUnderscore"), + new TestCaseData( + "__a__", + new[] { StringTokenType.DoubleUnderscore, StringTokenType.Text, StringTokenType.DoubleUnderscore } + ) + .SetName("TwoDoubleUnderScore_DividedBySingleLetterWord"), + new TestCaseData( + "__ __", + new[] { StringTokenType.DoubleUnderscore, StringTokenType.WhiteSpace, StringTokenType.DoubleUnderscore } + ) + .SetName("TwoDoubleUnderScore_DividedByWhitespace"), + new TestCaseData( + "__ __", + new[] { StringTokenType.DoubleUnderscore, StringTokenType.WhiteSpace, StringTokenType.DoubleUnderscore } + ) + .SetName("TwoDoubleUnderScore_DividedByWhitespaces"), + new TestCaseData( + "__abc__", + new[] { StringTokenType.DoubleUnderscore, StringTokenType.Text, StringTokenType.DoubleUnderscore } + ) + .SetName("TwoDoubleUnderScore_DividedByWord"), + new TestCaseData( + "___", + new[] { StringTokenType.DoubleUnderscore, StringTokenType.SingleUnderscore } + ) + .SetName("TwoDoubleUnderScoreAndUnderscoreInRow"), + new TestCaseData( + "# abc\n", + new[] { StringTokenType.Hash, StringTokenType.WhiteSpace, StringTokenType.Text, StringTokenType.NewLine } + ) + .SetName("HashWhitespaceWordAndNewLine"), + + }; +} \ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..d187860e8 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlDigit", "ControlDigi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{C3EF41D7-50EF-4CE1-B30A-D1D81C93D7FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{BBF05DDE-F868-48FB-9505-01C3A851AAD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownTest", "MarkdownTest\MarkdownTest.csproj", "{4062EC60-257D-4687-B172-FD327376B5B4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,5 +31,13 @@ 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 + {BBF05DDE-F868-48FB-9505-01C3A851AAD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBF05DDE-F868-48FB-9505-01C3A851AAD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBF05DDE-F868-48FB-9505-01C3A851AAD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBF05DDE-F868-48FB-9505-01C3A851AAD8}.Release|Any CPU.Build.0 = Release|Any CPU + {4062EC60-257D-4687-B172-FD327376B5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4062EC60-257D-4687-B172-FD327376B5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4062EC60-257D-4687-B172-FD327376B5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4062EC60-257D-4687-B172-FD327376B5B4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/cs/clean-code.sln.DotSettings b/cs/clean-code.sln.DotSettings index 135b83ecb..53fe49b2f 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" WarnAboutPrefixesAndSuffixes="False" 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" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True Imported 10.10.2016