diff --git a/.gitignore b/.gitignore index eaadbddaf..3fffa6f91 100644 --- a/.gitignore +++ b/.gitignore @@ -239,3 +239,4 @@ _Pvt_Extensions **/.idea **/.vscode **/node_modules +/cs/Markdown/Markdown.csproj.DotSettings diff --git a/cs/Markdown/AstNodes/BoldMarkdownNode.cs b/cs/Markdown/AstNodes/BoldMarkdownNode.cs new file mode 100644 index 000000000..aaf2b37ba --- /dev/null +++ b/cs/Markdown/AstNodes/BoldMarkdownNode.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.AstNodes; + +public class BoldMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren +{ + public override MarkdownNodeName Type => MarkdownNodeName.Bold; + public List Children { get; } = []; +} \ No newline at end of file diff --git a/cs/Markdown/AstNodes/HeadingMarkdownNode.cs b/cs/Markdown/AstNodes/HeadingMarkdownNode.cs new file mode 100644 index 000000000..994a2e04f --- /dev/null +++ b/cs/Markdown/AstNodes/HeadingMarkdownNode.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.AstNodes; + +public class HeadingMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren +{ + public override MarkdownNodeName Type => MarkdownNodeName.Heading; + public List Children { get; } = []; +} \ No newline at end of file diff --git a/cs/Markdown/AstNodes/IMarkdownNodeWithChildren.cs b/cs/Markdown/AstNodes/IMarkdownNodeWithChildren.cs new file mode 100644 index 000000000..ca1e5d39c --- /dev/null +++ b/cs/Markdown/AstNodes/IMarkdownNodeWithChildren.cs @@ -0,0 +1,6 @@ +namespace Markdown.AstNodes; + +public interface IMarkdownNodeWithChildren +{ + public List Children { get; } +} \ No newline at end of file diff --git a/cs/Markdown/AstNodes/ItalicMarkdownNode.cs b/cs/Markdown/AstNodes/ItalicMarkdownNode.cs new file mode 100644 index 000000000..692df2912 --- /dev/null +++ b/cs/Markdown/AstNodes/ItalicMarkdownNode.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.AstNodes; + +public class ItalicMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren +{ + public override MarkdownNodeName Type => MarkdownNodeName.Italic; + public List Children { get; } = []; +} \ No newline at end of file diff --git a/cs/Markdown/AstNodes/MarkdownNode.cs b/cs/Markdown/AstNodes/MarkdownNode.cs new file mode 100644 index 000000000..18e17e0ba --- /dev/null +++ b/cs/Markdown/AstNodes/MarkdownNode.cs @@ -0,0 +1,17 @@ +using Markdown.Enums; + +namespace Markdown.AstNodes; + +public abstract class MarkdownNode +{ + public abstract MarkdownNodeName Type { get; } + + public override bool Equals(object? obj) + { + if (this is IMarkdownNodeWithChildren node && obj is IMarkdownNodeWithChildren other) + return this.GetType() == other.GetType() && node.Children.SequenceEqual(other.Children); + if (this is TextMarkdownNode valueNode && obj is TextMarkdownNode otherValueNode) + return valueNode.Content.Equals(otherValueNode.Content); + return false; + } +} \ No newline at end of file diff --git a/cs/Markdown/AstNodes/RootMarkdownNode.cs b/cs/Markdown/AstNodes/RootMarkdownNode.cs new file mode 100644 index 000000000..96e5d6c5b --- /dev/null +++ b/cs/Markdown/AstNodes/RootMarkdownNode.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.AstNodes; + +public class RootMarkdownNode : MarkdownNode, IMarkdownNodeWithChildren +{ + public override MarkdownNodeName Type => MarkdownNodeName.Root; + public List Children { get; } = []; +} \ No newline at end of file diff --git a/cs/Markdown/AstNodes/TextMarkdownNode.cs b/cs/Markdown/AstNodes/TextMarkdownNode.cs new file mode 100644 index 000000000..039862a9c --- /dev/null +++ b/cs/Markdown/AstNodes/TextMarkdownNode.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.AstNodes; + +public class TextMarkdownNode(string content) : MarkdownNode +{ + public override MarkdownNodeName Type => MarkdownNodeName.Text; + public string Content => content; +} \ No newline at end of file diff --git a/cs/Markdown/Enums/MarkdownNodeName.cs b/cs/Markdown/Enums/MarkdownNodeName.cs new file mode 100644 index 000000000..6a59de752 --- /dev/null +++ b/cs/Markdown/Enums/MarkdownNodeName.cs @@ -0,0 +1,10 @@ +namespace Markdown.Enums; + +public enum MarkdownNodeName +{ + Bold, + Italic, + Heading, + Text, + Root, +} \ No newline at end of file diff --git a/cs/Markdown/Enums/MarkdownTokenName.cs b/cs/Markdown/Enums/MarkdownTokenName.cs new file mode 100644 index 000000000..2467027fc --- /dev/null +++ b/cs/Markdown/Enums/MarkdownTokenName.cs @@ -0,0 +1,12 @@ +namespace Markdown.Enums; + +public enum MarkdownTokenName +{ + Italic, + Bold, + Heading, + Text, + NewLine, + Space, + Number, +} \ No newline at end of file diff --git a/cs/Markdown/Interfaces/ILexer.cs b/cs/Markdown/Interfaces/ILexer.cs new file mode 100644 index 000000000..907fbd784 --- /dev/null +++ b/cs/Markdown/Interfaces/ILexer.cs @@ -0,0 +1,8 @@ +using Markdown.Tokens; + +namespace Markdown; + +public interface ILexer +{ + List Tokenize(string input); +} \ No newline at end of file diff --git a/cs/Markdown/Interfaces/IParser.cs b/cs/Markdown/Interfaces/IParser.cs new file mode 100644 index 000000000..63d820524 --- /dev/null +++ b/cs/Markdown/Interfaces/IParser.cs @@ -0,0 +1,9 @@ +using Markdown.AstNodes; +using Markdown.Tokens; + +namespace Markdown; + +public interface IParser +{ + RootMarkdownNode Parse(List 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..3a6353295 --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/cs/Markdown/MarkdownLexer.cs b/cs/Markdown/MarkdownLexer.cs new file mode 100644 index 000000000..dfb54f4e9 --- /dev/null +++ b/cs/Markdown/MarkdownLexer.cs @@ -0,0 +1,208 @@ +using System.Text; +using Markdown.Tokens; + +namespace Markdown; + +public class MarkdownLexer : ILexer +{ + private int position; + private readonly List tokens = []; + + private readonly char[] escapedChars = + [ + MarkdownSymbols.SharpChar, MarkdownSymbols.GroundChar, MarkdownSymbols.EscapeChar, MarkdownSymbols.NewLineChar + ]; + + public List Tokenize(string input) => Tokenize(new MarkdownLexerInput(input)); + + private List Tokenize(MarkdownLexerInput input) + { + position = 0; + var nestingStack = new Stack(); + + while (position < input.Length) + { + switch (input[position]) + { + case MarkdownSymbols.SpaceChar: + ParseSpaceAndAdvance(); + break; + case MarkdownSymbols.NewLineChar: + ParseNewLineAndAdvance(nestingStack); + break; + case MarkdownSymbols.EscapeChar: + ParseEscapeAndAdvance(input); + break; + case MarkdownSymbols.GroundChar: + ParseItalicOrBoldAndAdvance(input, nestingStack); + break; + case MarkdownSymbols.SharpChar: + ParseHeadingAndAdvance(input); + break; + default: + ParseTextAndAdvance(input); + break; + } + } + + return tokens; + } + + private void ParseSpaceAndAdvance() => tokens.Add(new SpaceToken(position++)); + + private void ParseHeadingAndAdvance(MarkdownLexerInput input) + { + if (input.NextIsSpace(position) && input.IsStartOfParagraph(position)) tokens.Add(new HeadingToken(position++)); + else tokens.Add(new TextToken(position, MarkdownSymbols.Sharp)); + position++; + } + + private void ParseTextAndAdvance(MarkdownLexerInput input) + { + var value = new StringBuilder(); + var start = position; + var endChars = new[] + { + MarkdownSymbols.SharpChar, MarkdownSymbols.GroundChar, MarkdownSymbols.NewLineChar, + MarkdownSymbols.EscapeChar, MarkdownSymbols.SpaceChar + }; + while (position < input.Length && !endChars.Contains(input[position]) && !input.CurrentIsDigit(position)) + value.Append(input[position++]); + + if (value.Length > 0) tokens.Add(new TextToken(start, value.ToString())); + if (position < input.Length && input.CurrentIsDigit(position)) ParseNumberAndAdvance(input); + } + + + private void ParseNumberAndAdvance(MarkdownLexerInput input) + { + var sb = new StringBuilder(); + var start = position; + while (position < input.Length && (input.CurrentIsDigit(position) || input[position] == MarkdownSymbols.GroundChar)) + sb.Append(input[position++]); + tokens.Add(new NumberToken(start, sb.ToString())); + } + + private void ParseItalicOrBoldAndAdvance(MarkdownLexerInput input, Stack stack) + { + var isDoubleGround = input.NextIsGround(position); + var isTripleGround = input.NextIsDoubleGround(position); + var isSingleGround = !isTripleGround && !isDoubleGround; + if (stack.Count == 0) ParseItalicOrBoldAndAdvanceWhenStackEmpty(isSingleGround, isTripleGround, stack); + else if (stack.Count == 1) + ParseItalicOrBoldAndAdvanceWhenStackHasOne(isSingleGround, isDoubleGround, isTripleGround, stack); + else if (stack.Count == 2) ParseItalicOrBoldAndAdvanceWhenStackHasTwo(isSingleGround, isTripleGround, stack); + } + + private void ParseItalicOrBoldAndAdvanceWhenStackEmpty(bool isSingleGround, bool isTripleGround, + Stack stack) + { + if (isSingleGround) + { + ParseItalicAndAdvance(); + stack.Push(MarkdownSymbols.Ground); + return; + } + + ParseBoldAndAdvance(); + stack.Push(MarkdownSymbols.DoubleGround); + if (!isTripleGround) return; + ParseItalicAndAdvance(); + stack.Push(MarkdownSymbols.Ground); + } + + private void ParseItalicOrBoldAndAdvanceWhenStackHasOne(bool isSingleGround, bool isDoubleGround, + bool isTripleGround, + Stack stack) + { + switch (stack.Peek()) + { + case MarkdownSymbols.DoubleGround when isSingleGround: + ParseItalicAndAdvance(); + stack.Push(MarkdownSymbols.Ground); + break; + case MarkdownSymbols.DoubleGround: + { + if (isTripleGround) ParseItalicAndAdvance(); + ParseBoldAndAdvance(); + stack.Pop(); + break; + } + case MarkdownSymbols.Ground: + { + if (isTripleGround) + { + ParseBoldAndAdvance(); + ParseItalicAndAdvance(); + } + else if (isDoubleGround) + { + tokens.Add(new TextToken(position, MarkdownSymbols.DoubleGround)); + position += 2; + } + else ParseItalicAndAdvance(); + + stack.Pop(); + break; + } + } + } + + private void ParseItalicOrBoldAndAdvanceWhenStackHasTwo(bool isSingleGround, bool isTripleGround, + Stack stack) + { + if (isSingleGround) + { + ParseItalicAndAdvance(); + stack.Pop(); + return; + } + + if (isTripleGround) ParseItalicAndAdvance(); + ParseBoldAndAdvance(); + + stack.Pop(); + stack.Pop(); + } + + private void ParseBoldAndAdvance() + { + tokens.Add(new BoldToken(position)); + position += 2; + } + + private void ParseItalicAndAdvance() + { + tokens.Add(new ItalicToken(position)); + position++; + } + + private void ParseNewLineAndAdvance(Stack stack) + { + tokens.Add(new NewLineToken(position)); + stack.Clear(); + position++; + } + + private void ParseEscapeAndAdvance(MarkdownLexerInput input) + { + if (position + 1 >= input.Length) + { + tokens.Add(new TextToken(position++, MarkdownSymbols.Escape)); + return; + } + + if (input.NextIsDoubleGround(position)) + { + tokens.Add(new TextToken(position, MarkdownSymbols.DoubleGround)); + position += 3; + return; + } + + var next = input[position + 1]; + tokens.Add(escapedChars.Contains(next) + ? new TextToken(position, next.ToString()) + : new TextToken(position, MarkdownSymbols.Escape + next)); + position += 2; + } +} \ No newline at end of file diff --git a/cs/Markdown/MarkdownLexerInput.cs b/cs/Markdown/MarkdownLexerInput.cs new file mode 100644 index 000000000..6da318635 --- /dev/null +++ b/cs/Markdown/MarkdownLexerInput.cs @@ -0,0 +1,22 @@ +namespace Markdown; + +public class MarkdownLexerInput(string input) +{ + public bool NextIsDoubleGround(int position) => + position + 2 < input.Length && input[position + 1] == MarkdownSymbols.GroundChar && + input[position + 2] == MarkdownSymbols.GroundChar; + + public bool NextIsSpace(int position) => + position + 1 < input.Length && input[position + 1] == MarkdownSymbols.SpaceChar; + + public bool NextIsGround(int position) => + position + 1 < input.Length && input[position + 1] == MarkdownSymbols.GroundChar; + + public bool CurrentIsDigit(int position) => char.IsDigit(input[position]); + + public bool IsStartOfParagraph(int position) => + position == 0 || position > 0 && input[position - 1] == MarkdownSymbols.NewLineChar; + + public char this[int index] => input[index]; + public int Length => input.Length; +} \ No newline at end of file diff --git a/cs/Markdown/MarkdownParser.cs b/cs/Markdown/MarkdownParser.cs new file mode 100644 index 000000000..e65728db6 --- /dev/null +++ b/cs/Markdown/MarkdownParser.cs @@ -0,0 +1,207 @@ +using Markdown.AstNodes; +using Markdown.Enums; +using Markdown.Tokens; + +namespace Markdown; + +public class MarkdownParser : IParser +{ + private const MarkdownTokenName Text = MarkdownTokenName.Text; + private const MarkdownTokenName Bold = MarkdownTokenName.Bold; + private const MarkdownTokenName Italic = MarkdownTokenName.Italic; + private const MarkdownTokenName NewLine = MarkdownTokenName.NewLine; + private const MarkdownTokenName Space = MarkdownTokenName.Space; + + public RootMarkdownNode Parse(List tokens) + { + var root = new RootMarkdownNode(); + ParseChildren(tokens, root, 0, tokens.Count); + return root; + } + + private void ParseChildren(List tokens, IMarkdownNodeWithChildren parent, int left, int right) + { + if (left < 0 || right > tokens.Count) return; + if (left >= right) return; + var index = left; + while (index >= left && index < right) + { + var token = tokens[index]; + switch (token) + { + case TextToken: + case SpaceToken: + case NewLineToken: + case NumberToken: + parent.Children.Add(new TextMarkdownNode(token.Value)); + index++; + break; + case HeadingToken: + { + var heading = new HeadingMarkdownNode(); + var next = FindIndexOfCloseHeadingToken(tokens, index); + ParseChildren(tokens, heading, index + 1, next == -1 ? right : next); + parent.Children.Add(heading); + index = next == -1 ? right : next; + break; + } + case ItalicToken: + { + index = ParseItalicWithChildren(tokens, parent, index, right); + break; + } + case BoldToken: + { + index = ParseBoldWithChildren(tokens, parent, index); + break; + } + } + } + } + + private int ParseItalicWithChildren(List tokens, IMarkdownNodeWithChildren parent, int start, int right) + { + var italic = new ItalicMarkdownNode(); + var next = FindIndexOfCloseItalicToken(tokens, start); + + if (next == -1 || next >= right) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + return start + 1; + } + + if (parent is ItalicMarkdownNode) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + ParseChildren(tokens, parent, start + 1, next); + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + return next + 1; + } + + if (TokenInWord(tokens, start) && TokenInWord(tokens, next) && + ContainsToken(tokens, MarkdownTokenName.Space, start, next)) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + for (var j = start + 1; j < next; j++) parent.Children.Add(new TextMarkdownNode(tokens[j].Value)); + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + return next + 1; + } + + ParseChildren(tokens, italic, start + 1, next); + if (italic.Children.Count == 0) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground + MarkdownSymbols.Ground)); + return start + 2; + } + + parent.Children.Add(italic); + return next + 1; + } + + private int ParseBoldWithChildren(List tokens, IMarkdownNodeWithChildren parent, int i) + { + var bold = new BoldMarkdownNode(); + var next = FindIndexOfCloseBoldToken(tokens, i); + if (next == -1 || parent is ItalicMarkdownNode) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.DoubleGround)); + return i + 1; + } + + var indexOfIntersection = FindIndexOfIntersection(tokens, i + 1, next); + if (indexOfIntersection.start > 0) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.DoubleGround)); + ParseChildren(tokens, parent, i + 1, indexOfIntersection.start); + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + ParseChildren(tokens, parent, indexOfIntersection.start + 1, next); + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.DoubleGround)); + ParseChildren(tokens, parent, next + 1, indexOfIntersection.end); + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.Ground)); + return indexOfIntersection.end + 1; + } + + ParseChildren(tokens, bold, i + 1, next); + if (bold.Children.Count == 0) + { + parent.Children.Add(new TextMarkdownNode(MarkdownSymbols.DoubleGround + MarkdownSymbols.DoubleGround)); + return i + 2; + } + + parent.Children.Add(bold); + return next + 1; + } + + private int FindIndexOfCloseItalicToken(List tokens, int start) + { + var index = start + 1; + if (index < tokens.Count && tokens[index].Is(Space)) return -1; + while (index < tokens.Count && tokens[index].Name != NewLine) + { + if (!tokens[index].Is(Italic)) + { + index++; + continue; + } + + if (index + 1 < tokens.Count && tokens[index + 1].Is(Italic)) + { + index += 2; + continue; + } + + if (index > 0 && !tokens[index - 1].Is(Space)) return index; + index++; + } + + return -1; + } + + private int FindIndexOfCloseBoldToken(List tokens, int start) + { + var index = start + 1; + if (index >= tokens.Count || tokens[index].Is(Space)) return -1; + while (index < tokens.Count && tokens[index].Name != NewLine) + { + if (index > 0 && tokens[index].Is(Bold) && !tokens[index - 1].Is(Space)) + return index; + index++; + } + + return -1; + } + + private int FindIndexOfCloseHeadingToken(List tokens, int start) + { + var index = start; + while (index < tokens.Count && !tokens[index].Is(NewLine)) + index++; + return index == tokens.Count ? -1 : index; + } + + private (int start, int end) FindIndexOfIntersection(List tokens, int left, int right) + { + for (var i = left; i < right; i++) + if (tokens[i] is ItalicToken) + { + var end = FindIndexOfCloseItalicToken(tokens, i); + if (end > right) return (i, end); + if (end == -1) continue; + i = end + 1; + } + + return (-1, -1); + } + + private bool TokenInWord(List tokens, int index) + => index > 0 && tokens[index - 1].Is(Text) && index + 1 < tokens.Count && + tokens[index + 1].Is(Text); + + private bool ContainsToken(List tokens, MarkdownTokenName expected, int left, int right) + { + for (var i = left; i < right; i++) + if (tokens[i].Is(expected)) + return true; + return false; + } +} \ No newline at end of file diff --git a/cs/Markdown/MarkdownSymbols.cs b/cs/Markdown/MarkdownSymbols.cs new file mode 100644 index 000000000..0b4132f04 --- /dev/null +++ b/cs/Markdown/MarkdownSymbols.cs @@ -0,0 +1,14 @@ +namespace Markdown; + +public static class MarkdownSymbols +{ + public const string DoubleGround = "__"; + public const string Ground = "_"; + public const string Sharp = "#"; + public const string Escape = "\\"; + public const char GroundChar = '_'; + public const char SharpChar = '#'; + public const char EscapeChar = '\\'; + public const char NewLineChar = '\n'; + public const char SpaceChar = ' '; +} \ No newline at end of file diff --git a/cs/Markdown/MarkdownToHtmlConverter.cs b/cs/Markdown/MarkdownToHtmlConverter.cs new file mode 100644 index 000000000..ac909b9ef --- /dev/null +++ b/cs/Markdown/MarkdownToHtmlConverter.cs @@ -0,0 +1,58 @@ +using System.Text; +using Markdown.AstNodes; + +namespace Markdown; + +public class MarkdownToHtmlConverter(ILexer lexer, IParser parser) +{ + private ILexer Lexer { get; } = lexer; + private IParser Parser { get; } = parser; + + public string Convert(string input) + { + var tokens = Lexer.Tokenize(input); + var ast = Parser.Parse(tokens); + return ConvertAstToHtml(ast); + } + + private string ConvertAstToHtml(RootMarkdownNode ast) + { + var html = new StringBuilder(); + ConvertToHtml(ast, html); + return html.ToString(); + } + + private void ConvertToHtml(MarkdownNode node, StringBuilder html) + { + switch (node) + { + case TextMarkdownNode textNode: + html.Append(textNode.Content); + break; + case ItalicMarkdownNode italicNode: + html.Append(""); + foreach (var child in italicNode.Children) + ConvertToHtml(child, html); + html.Append(""); + break; + case BoldMarkdownNode boldNode: + html.Append(""); + foreach (var child in boldNode.Children) + ConvertToHtml(child, html); + html.Append(""); + break; + case HeadingMarkdownNode headingNode: + html.Append("

"); + foreach (var child in headingNode.Children) + ConvertToHtml(child, html); + html.Append("

"); + break; + case RootMarkdownNode root: + { + foreach (var child in root.Children) + ConvertToHtml(child, html); + break; + } + } + } +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/BoldToken.cs b/cs/Markdown/Tokens/BoldToken.cs new file mode 100644 index 000000000..58c6b2bdb --- /dev/null +++ b/cs/Markdown/Tokens/BoldToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class BoldToken(int position) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.Bold; + public override string Value => "__"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/HeadingToken.cs b/cs/Markdown/Tokens/HeadingToken.cs new file mode 100644 index 000000000..3b4c7ee70 --- /dev/null +++ b/cs/Markdown/Tokens/HeadingToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class HeadingToken(int position) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.Heading; + public override string Value => "# "; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/ItalicToken.cs b/cs/Markdown/Tokens/ItalicToken.cs new file mode 100644 index 000000000..c48dbadcf --- /dev/null +++ b/cs/Markdown/Tokens/ItalicToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class ItalicToken(int position) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.Italic; + public override string Value => "_"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/NewLineToken.cs b/cs/Markdown/Tokens/NewLineToken.cs new file mode 100644 index 000000000..934e882b7 --- /dev/null +++ b/cs/Markdown/Tokens/NewLineToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class NewLineToken(int position) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.NewLine; + public override string Value => "\n"; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/NumberToken.cs b/cs/Markdown/Tokens/NumberToken.cs new file mode 100644 index 000000000..72cbcb0b9 --- /dev/null +++ b/cs/Markdown/Tokens/NumberToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class NumberToken(int position, string value) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.Number; + public override string Value => value; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/SpaceToken.cs b/cs/Markdown/Tokens/SpaceToken.cs new file mode 100644 index 000000000..371179f32 --- /dev/null +++ b/cs/Markdown/Tokens/SpaceToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class SpaceToken(int position) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.Space; + public override string Value => " "; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/TextToken.cs b/cs/Markdown/Tokens/TextToken.cs new file mode 100644 index 000000000..2e3f88b97 --- /dev/null +++ b/cs/Markdown/Tokens/TextToken.cs @@ -0,0 +1,9 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public class TextToken(int position, string value) : Token(position) +{ + public override MarkdownTokenName Name => MarkdownTokenName.Text; + public override string Value => value; +} \ No newline at end of file diff --git a/cs/Markdown/Tokens/Token.cs b/cs/Markdown/Tokens/Token.cs new file mode 100644 index 000000000..4833b081d --- /dev/null +++ b/cs/Markdown/Tokens/Token.cs @@ -0,0 +1,18 @@ +using Markdown.Enums; + +namespace Markdown.Tokens; + +public abstract class Token(int position) +{ + public abstract MarkdownTokenName Name { get; } + public abstract string Value { get; } + public int Position => position; + public int Length => Value.Length; + public bool Is(MarkdownTokenName type) => type == Name; + + public override bool Equals(object? obj) => obj is Token token && Equals(token); + + public override int GetHashCode() => HashCode.Combine((int)Name, Value); + + private bool Equals(Token token) => Name == token.Name && Position == token.Position && Value == token.Value; +} \ No newline at end of file diff --git a/cs/MarkdownTests/LexerTests.cs b/cs/MarkdownTests/LexerTests.cs new file mode 100644 index 000000000..3bcdf060d --- /dev/null +++ b/cs/MarkdownTests/LexerTests.cs @@ -0,0 +1,99 @@ +using FluentAssertions; +using Markdown; +using Markdown.Tokens; +using NUnit.Framework; + +namespace MarkdownTests; + +public class LexerTests +{ + private MarkdownLexer lexer; + + [SetUp] + public void Setup() => lexer = new MarkdownLexer(); + + [Test] + public void Tokenize_WorksCorrect_WhenItalic() + { + const string text = "_italic_"; + var expected = new Token[] { new ItalicToken(0), new TextToken(1, "italic"), new ItalicToken(7) }; + var actual = lexer.Tokenize(text); + actual.Should().BeEquivalentTo(expected, o => o.WithStrictOrdering()); + } + + [Test] + public void Tokenize_WorksCorrect_WhenBold() + { + const string text = "__bold__"; + var expected = new Token[] { new BoldToken(0), new TextToken(2, "bold"), new BoldToken(6) }; + var actual = lexer.Tokenize(text); + actual.Should().BeEquivalentTo(expected, o => o.WithStrictOrdering()); + } + + [Test] + public void Tokenize_WorksCorrect_WhenHeadingWithoutCloseTag() + { + const string text = "# heading"; + var expected = new Token[] { new HeadingToken(0), new TextToken(2, "heading") }; + var actual = lexer.Tokenize(text); + actual.Should().BeEquivalentTo(expected, o => o.WithStrictOrdering()); + } + + [Test] + public void Tokenize_WorksCorrect_WhenHeadingWithCloseTag() + { + const string text = "# heading\ntext"; + var expected = new Token[] + { + new HeadingToken(0), new TextToken(2, "heading"), new NewLineToken(9), new TextToken(10, "text") + }; + var actual = lexer.Tokenize(text); + actual.Should().BeEquivalentTo(expected, o => o.WithStrictOrdering()); + } + + [Test] + public void Tokenize_WorksCorrect_WithItalicInBold() + { + const string text = "__bold _italic___"; + var expected = new Token[] + { + new BoldToken(0), new TextToken(2, "bold"), new SpaceToken(6), new ItalicToken(7), + new TextToken(8, "italic"), new ItalicToken(14), new BoldToken(15) + }; + var actual = lexer.Tokenize(text); + actual.Should().BeEquivalentTo(expected, o => o.WithStrictOrdering()); + } + + [Test] + public void Tokenize_WorksCorrect_WithAllTags() + { + const string text = "# a b\\_c _d_ __e__\n___f___ 1_234"; + var expected = new Token[] + { + new HeadingToken(0), + new TextToken(2, "a"), + new SpaceToken(3), + new TextToken(4, "b"), + new TextToken(5, @"_"), + new TextToken(7, @"c"), + new SpaceToken(8), + new ItalicToken(9), + new TextToken(10, "d"), + new ItalicToken(11), + new SpaceToken(12), + new BoldToken(13), + new TextToken(15, "e"), + new BoldToken(16), + new NewLineToken(18), + new BoldToken(19), + new ItalicToken(21), + new TextToken(22, "f"), + new ItalicToken(23), + new BoldToken(24), + new SpaceToken(26), + new NumberToken(27, "1_234") + }; + var actual = lexer.Tokenize(text); + actual.Should().BeEquivalentTo(expected, o => o.WithStrictOrdering()); + } +} \ No newline at end of file diff --git a/cs/MarkdownTests/MarkdownConverterTests.cs b/cs/MarkdownTests/MarkdownConverterTests.cs new file mode 100644 index 000000000..71e95ae8c --- /dev/null +++ b/cs/MarkdownTests/MarkdownConverterTests.cs @@ -0,0 +1,103 @@ +using System.Diagnostics; +using System.Text; +using FluentAssertions; +using Markdown; +using NUnit.Framework; + +namespace MarkdownTests; + +[TestFixture] +public class MarkdownConverterTests +{ + MarkdownToHtmlConverter converter; + + [SetUp] + public void Setup() + { + var lexer = new MarkdownLexer(); + var parser = new MarkdownParser(); + converter = new MarkdownToHtmlConverter(lexer, parser); + } + + [Test] + public void ConvertMarkdownToHtml() + { + var md = "# title"; + var expected = "

title

"; + var actual = converter.Convert(md); + actual.Should().Be(expected); + } + + [TestCase("# header", "

header

")] + [TestCase("_italic_", "italic")] + [TestCase("ita_lic_", "italic")] + [TestCase("__strong__", "strong")] + [TestCase("st__rong__", "strong")] + [TestCase("___text___", "text")] + [TestCase("__text _text_ text__", "text text text")] + [TestCase("# header\n new line", "

header

\n new line")] + [TestCase(@"\n\_Вот это\_", @"\n_Вот это_")] + [TestCase("line with _italic_ text", "line with italic text")] + [TestCase("a _t_ b", "a t b")] + [TestCase("line with __strong__ text", "line with strong text")] + [TestCase("line with __text _text_ text__ abc", "line with text text text abc")] + [TestCase("# Header 1\n ___Dear Diary___, today has been a _hard_ day", + "

Header 1

\n Dear Diary, today has been a hard day")] + [TestCase("# _Header_ 1\n ___Dear Diary___, today has been a _hard_ day", + "

Header 1

\n Dear Diary, today has been a hard day")] + public void MdRender_ReturnsExpectedHtml(string md, string expected) + { + var actual = converter.Convert(md); + actual.Should().Be(expected); + } + + [Test] + public void ConvertMarkdownToHtml_ConformsToSpecification() + { + var dir = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestsData"); + var expectedPath = Path.Combine(dir, "expected.txt"); + var md = File.ReadAllText(Path.Combine(dir, "test.txt")); + + var expected = File.ReadAllText(expectedPath); + var actual = converter.Convert(md); + + actual.Should().Be(expected); + } + + [Test] + public void Convert_ShouldPerformInLinearTime() + { + const int smallInputSize = 1000; + const int largeInputSize = 100000; + + var smallInput = GenerateMarkdownInput(smallInputSize); + var largeInput = GenerateMarkdownInput(largeInputSize); + + var smallTime = MeasureExecutionTime(() => converter.Convert(smallInput)); + var largeTime = MeasureExecutionTime(() => converter.Convert(largeInput)); + + var growthFactor = (double)largeTime / smallTime; + growthFactor.Should().BeLessThan(largeInputSize / smallInputSize * 1.5, "execution time should grow linearly with the size of the input"); + } + + private string GenerateMarkdownInput(int size) + { + var mdBuilder = new StringBuilder(size); + for (var i = 0; i < size; i++) + { + mdBuilder.Append("# Heading\n"); + mdBuilder.Append("**Bold text**\n"); + mdBuilder.Append("*Italic text*\n"); + } + + return mdBuilder.ToString(); + } + + private long MeasureExecutionTime(Action action) + { + var stopwatch = Stopwatch.StartNew(); + action(); + stopwatch.Stop(); + return stopwatch.ElapsedMilliseconds; + } +} \ No newline at end of file diff --git a/cs/MarkdownTests/MarkdownTests.csproj b/cs/MarkdownTests/MarkdownTests.csproj new file mode 100644 index 000000000..240a9d459 --- /dev/null +++ b/cs/MarkdownTests/MarkdownTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + Exe + + + + bin\Debug/ + + + + bin\Release/ + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/cs/MarkdownTests/ParserTests.cs b/cs/MarkdownTests/ParserTests.cs new file mode 100644 index 000000000..441754fd4 --- /dev/null +++ b/cs/MarkdownTests/ParserTests.cs @@ -0,0 +1,163 @@ +using FluentAssertions; +using Markdown; +using Markdown.AstNodes; +using Markdown.Enums; +using NUnit.Framework; + +namespace MarkdownTests; + +public class ParserTests +{ + private MarkdownParser parser; + private MarkdownLexer lexer; + + [SetUp] + public void Setup() + { + parser = new MarkdownParser(); + lexer = new MarkdownLexer(); + } + + [Test] + [Description("Ручная проверка корректности AST для тегов Heading, Italic, Bold, Text")] + public void Parse_ReturnsAst_WithAllTags() + { + const string md = "# a b\\_c _d_ __e__\n___f___ 1_234"; + var tokens = lexer.Tokenize(md); + var actual = parser.Parse(tokens); + + var space = new TextMarkdownNode(" "); + var newLine = new TextMarkdownNode("\n"); + + var heading = new HeadingMarkdownNode(); + heading.Children.Add(new TextMarkdownNode("a")); + heading.Children.Add(space); + heading.Children.Add(new TextMarkdownNode("b")); + heading.Children.Add(new TextMarkdownNode("_")); + heading.Children.Add(new TextMarkdownNode("c")); + heading.Children.Add(space); + + var italicD = new ItalicMarkdownNode(); + italicD.Children.Add(new TextMarkdownNode("d")); + + heading.Children.Add(italicD); + heading.Children.Add(space); + + var boldE = new BoldMarkdownNode(); + boldE.Children.Add(new TextMarkdownNode("e")); + + heading.Children.Add(boldE); + + var boldF = new BoldMarkdownNode(); + var italicF = new ItalicMarkdownNode(); + italicF.Children.Add(new TextMarkdownNode("f")); + boldF.Children.Add(italicF); + + var text1_234 = new TextMarkdownNode("1_234"); + + var expected = new RootMarkdownNode(); + expected.Children.Add(heading); + expected.Children.Add(newLine); + expected.Children.Add(boldF); + expected.Children.Add(space); + expected.Children.Add(text1_234); + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + [Description("Глубина дерева должна быть не больше 5: Root->Heading->Bold->Italic->Text")] + public void Parse_ReturnsAst_WithDepthLessThanFive() + { + var dir = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestsData"); + var md = File.ReadAllText(Path.Combine(dir, "test.txt")); + var tokens = lexer.Tokenize(md); + var ast = parser.Parse(tokens); + var depth = GetAstDepth(ast); + depth.Should().BeLessThanOrEqualTo(5); + } + + [Test] + [Description("Вложенность должна быть корректной: Root->Heading->Bold->Italic->Text")] + public void Parse_ReturnsAst_WithCorrectNesting() + { + var dir = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestsData"); + var md = File.ReadAllText(Path.Combine(dir, "test.txt")); + var tokens = lexer.Tokenize(md); + var ast = parser.Parse(tokens); + CheckNesting(ast); + } + + [Test] + public void Parse_Ast_NotHaveEmptyItalic() + { + var dir = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestsData"); + var md = File.ReadAllText(Path.Combine(dir, "test.txt")); + var tokens = lexer.Tokenize(md); + var ast = parser.Parse(tokens); + AstNotHaveEmpty(ast); + } + + [Test] + public void Parse_Ast_NotHaveEmptyBold() + { + var dir = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestsData"); + var md = File.ReadAllText(Path.Combine(dir, "test.txt")); + var tokens = lexer.Tokenize(md); + var ast = parser.Parse(tokens); + AstNotHaveEmpty(ast); + } + + private void AstNotHaveEmpty(MarkdownNode node) where TDisallowed : MarkdownNode + { + if (node is TextMarkdownNode) return; + if (node is IMarkdownNodeWithChildren nodeWithChildren) + { + if (nodeWithChildren is TDisallowed) + nodeWithChildren.Children.Should().NotBeEmpty(); + foreach (var child in nodeWithChildren.Children) + AstNotHaveEmpty(child); + } + else throw new ArgumentException($"Not expected node type: {node.Type}"); + } + + private int GetAstDepth(MarkdownNode node, int level = 1) + { + var maxLevel = level; + if (node is not IMarkdownNodeWithChildren nodeWithChildren) return maxLevel; + foreach (var child in nodeWithChildren.Children) + maxLevel = Math.Max(level, GetAstDepth(child, level + 1)); + return maxLevel; + } + + private void CheckNesting(MarkdownNode node) + { + if (node is TextMarkdownNode) return; + if (node is IMarkdownNodeWithChildren nodeWithChildren) + { + var childrenTypes = nodeWithChildren.Children.Select(n => n.Type).ToHashSet(); + var allowedTypes = GetAllowedChildrenFor(node); + + foreach (var type in childrenTypes) + allowedTypes.Should().Contain(type); + + foreach (var child in nodeWithChildren.Children) + CheckNesting(child); + } + else throw new ArgumentException($"Not expected node type: {node.Type}"); + } + + private MarkdownNodeName[] GetAllowedChildrenFor(MarkdownNode node) + { + return node switch + { + RootMarkdownNode => + [MarkdownNodeName.Bold, MarkdownNodeName.Italic, MarkdownNodeName.Text, MarkdownNodeName.Heading], + HeadingMarkdownNode => [MarkdownNodeName.Bold, MarkdownNodeName.Italic, MarkdownNodeName.Text], + BoldMarkdownNode => [MarkdownNodeName.Italic, MarkdownNodeName.Text], + ItalicMarkdownNode => [MarkdownNodeName.Text], + TextMarkdownNode => [], + _ => throw new ArgumentException($"Not expected node type: {node.Type}") + }; + } +} \ No newline at end of file diff --git a/cs/MarkdownTests/TestsData/expected.txt b/cs/MarkdownTests/TestsData/expected.txt new file mode 100644 index 000000000..350d75b40 --- /dev/null +++ b/cs/MarkdownTests/TestsData/expected.txt @@ -0,0 +1,73 @@ +

Спецификация языка разметки

+ +Посмотрите этот файл в сыром виде. Сравните с тем, что показывает github. +Все совпадения случайны ;) + + + +

Курсив

+ +Текст, окруженный с двух сторон одинарными символами подчерка, +должен помещаться в HTML-тег \ вот так: + +Текст, \окруженный с двух сторон\ одинарными символами подчерка, +должен помещаться в HTML-тег \. + + + +

Полужирный

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

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

+ +Любой символ можно экранировать, чтобы он не считался частью разметки. +_Вот это_, не должно выделиться тегом \. + +Символ экранирования исчезает из результата, только если экранирует что-то. +Здесь сим\волы экранирования\ \должны остаться. + +Символ экранирования тоже можно экранировать: \вот это будет выделено тегом \ + + + +

Взаимодействие тегов

+ +Внутри двойного выделения одинарное тоже работает. + +Но не наоборот — внутри одинарного __двойное__ не работает. + +Подчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка. + +Однако выделять часть слова они могут: и в начале, и в середине, и в конце. + +В то же время выделение в ра_зных сл_овах не работает. + +__Непарные_ символы в рамках одного абзаца не считаются выделением. + +За подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением +и остаются просто символами подчерка. + +Подчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти подчерки _не считаются окончанием выделения +и остаются просто символами подчерка. + +В случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением. + +Если внутри подчерков пустая строка ____, то они остаются символами подчерка. + + + +

Заголовки

+ +Абзац, начинающийся с "# ", выделяется тегом \

в заголовок. +В тексте заголовка могут присутствовать все прочие символы разметки с указанными правилами. + +Таким образом + +

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

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

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

\ No newline at end of file diff --git a/cs/MarkdownTests/TestsData/test.txt b/cs/MarkdownTests/TestsData/test.txt new file mode 100644 index 000000000..886e99c95 --- /dev/null +++ b/cs/MarkdownTests/TestsData/test.txt @@ -0,0 +1,73 @@ +# Спецификация языка разметки + +Посмотрите этот файл в сыром виде. Сравните с тем, что показывает github. +Все совпадения случайны ;) + + + +# Курсив + +Текст, _окруженный с двух сторон_ одинарными символами подчерка, +должен помещаться в HTML-тег \ вот так: + +Текст, \окруженный с двух сторон\ одинарными символами подчерка, +должен помещаться в HTML-тег \. + + + +# Полужирный + +__Выделенный двумя символами текст__ должен становиться полужирным с помощью тега \. + + + +# Экранирование + +Любой символ можно экранировать, чтобы он не считался частью разметки. +\_Вот это\_, не должно выделиться тегом \. + +Символ экранирования исчезает из результата, только если экранирует что-то. +Здесь сим\волы экранирования\ \должны остаться.\ + +Символ экранирования тоже можно экранировать: \\_вот это будет выделено тегом_ \ + + + +# Взаимодействие тегов + +Внутри __двойного выделения _одинарное_ тоже__ работает. + +Но не наоборот — внутри _одинарного __двойное__ не_ работает. + +Подчерки внутри текста c цифрами_12_3 не считаются выделением и должны оставаться символами подчерка. + +Однако выделять часть слова они могут: и в _нач_але, и в сер_еди_не, и в кон_це._ + +В то же время выделение в ра_зных сл_овах не работает. + +__Непарные_ символы в рамках одного абзаца не считаются выделением. + +За подчерками, начинающими выделение, должен следовать непробельный символ. Иначе эти_ подчерки_ не считаются выделением +и остаются просто символами подчерка. + +Подчерки, заканчивающие выделение, должны следовать за непробельным символом. Иначе эти _подчерки _не считаются_ окончанием выделения +и остаются просто символами подчерка. + +В случае __пересечения _двойных__ и одинарных_ подчерков ни один из них не считается выделением. + +Если внутри подчерков пустая строка ____, то они остаются символами подчерка. + + + +# Заголовки + +Абзац, начинающийся с "# ", выделяется тегом \

в заголовок. +В тексте заголовка могут присутствовать все прочие символы разметки с указанными правилами. + +Таким образом + +# Заголовок __с _разными_ символами__ + +превратится в: + +\

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

\ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..e221462d2 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", "{05E17013-A047-4ACD-ABC6-C3777A6AFCEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownTests", "MarkdownTests\MarkdownTests.csproj", "{B647A53B-1176-4E76-9043-550EFB336372}" +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 + {05E17013-A047-4ACD-ABC6-C3777A6AFCEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05E17013-A047-4ACD-ABC6-C3777A6AFCEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05E17013-A047-4ACD-ABC6-C3777A6AFCEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05E17013-A047-4ACD-ABC6-C3777A6AFCEC}.Release|Any CPU.Build.0 = Release|Any CPU + {B647A53B-1176-4E76-9043-550EFB336372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B647A53B-1176-4E76-9043-550EFB336372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B647A53B-1176-4E76-9043-550EFB336372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B647A53B-1176-4E76-9043-550EFB336372}.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..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