From ce5fffa63e45ba1ff3b9dc50c10bfe0105e3143d Mon Sep 17 00:00:00 2001 From: Ruslan Date: Thu, 30 Oct 2025 23:02:39 +0500 Subject: [PATCH 01/19] Classwork --- cs/Chess/ChessProblem.cs | 56 ++++++++++++++---------- cs/Chess/ChessProblem_Test.cs | 18 ++++---- cs/Chess/TemporaryPieceMove.cs | 2 + cs/ControlDigit/Snils/SnilsExtensions.cs | 20 ++++++++- cs/ControlDigit/Upc/UpcExtensions.cs | 47 +++++++++++++++++++- 5 files changed, 109 insertions(+), 34 deletions(-) diff --git a/cs/Chess/ChessProblem.cs b/cs/Chess/ChessProblem.cs index f0d6ab430..719dc6b34 100644 --- a/cs/Chess/ChessProblem.cs +++ b/cs/Chess/ChessProblem.cs @@ -2,42 +2,50 @@ { public class ChessProblem { - private static Board board; - public static ChessStatus ChessStatus; + private Board board; + public ChessStatus ChessStatus; - public static void LoadFrom(string[] lines) + public ChessProblem(Board newBoard) { - board = new BoardParser().ParseBoard(lines); + board = newBoard; } // Определяет мат, шах или пат белым. - public static void CalculateChessStatus() + public ChessStatus CalculateChessStatus() { var isCheck = IsCheckForWhite(); - var hasMoves = false; + var whiteKingHasMoves = false; foreach (var locFrom in board.GetPieces(PieceColor.White)) { foreach (var locTo in board.GetPiece(locFrom).GetMoves(locFrom, board)) { - var old = board.GetPiece(locTo); - board.Set(locTo, board.GetPiece(locFrom)); - board.Set(locFrom, null); + var temporaryMove = board.PerformTemporaryMove(locFrom, locTo); + if (!IsCheckForWhite()) - hasMoves = true; - board.Set(locFrom, board.GetPiece(locTo)); - board.Set(locTo, old); + whiteKingHasMoves = true; + + temporaryMove.Undo(); } } - if (isCheck) - if (hasMoves) - ChessStatus = ChessStatus.Check; - else ChessStatus = ChessStatus.Mate; - else if (hasMoves) ChessStatus = ChessStatus.Ok; - else ChessStatus = ChessStatus.Stalemate; + + if (isCheck && whiteKingHasMoves) + { + ChessStatus = ChessStatus.Check; + } + else if (isCheck && !whiteKingHasMoves) + { + ChessStatus = ChessStatus.Mate; + } + else if (!isCheck && !whiteKingHasMoves) + { + ChessStatus = ChessStatus.Stalemate; + } + else ChessStatus = ChessStatus.Ok; + return ChessStatus; } // check — это шах - private static bool IsCheckForWhite() + private bool IsCheckForWhite() { var isCheck = false; foreach (var loc in board.GetPieces(PieceColor.Black)) @@ -46,13 +54,13 @@ private static bool IsCheckForWhite() var moves = piece.GetMoves(loc, board); foreach (var destination in moves) { - if (Piece.Is(board.GetPiece(destination), - PieceColor.White, PieceType.King)) - isCheck = true; + if (Piece.Is(board.GetPiece(destination), PieceColor.White, PieceType.King)) + { + isCheck = true; + } } } - if (isCheck) return true; - return false; + return isCheck; } } } \ No newline at end of file diff --git a/cs/Chess/ChessProblem_Test.cs b/cs/Chess/ChessProblem_Test.cs index bb3915771..5cd0ec328 100644 --- a/cs/Chess/ChessProblem_Test.cs +++ b/cs/Chess/ChessProblem_Test.cs @@ -21,13 +21,15 @@ public void RepeatedMethodCallDoNotChangeBehaviour() " ", " ", }; - ChessProblem.LoadFrom(boardLines); - ChessProblem.CalculateChessStatus(); - Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); + + var board = new BoardParser().ParseBoard(boardLines); + var chessProblem = new ChessProblem(board); + var chessStatus = chessProblem.CalculateChessStatus(); + Assert.AreEqual(ChessStatus.Check, chessStatus); // Now check that internal board modifications during the first call do not change answer - ChessProblem.CalculateChessStatus(); - Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); + chessStatus = chessProblem.CalculateChessStatus(); + Assert.AreEqual(ChessStatus.Check, chessStatus); } [Test] @@ -46,10 +48,10 @@ public void AllTests() private static void TestOnFile(string filename) { var boardLines = File.ReadAllLines(filename); - ChessProblem.LoadFrom(boardLines); + var board = new BoardParser().ParseBoard(boardLines); + var chessProblem = new ChessProblem(board); var expectedAnswer = File.ReadAllText(Path.ChangeExtension(filename, ".ans")).Trim(); - ChessProblem.CalculateChessStatus(); - Assert.AreEqual(expectedAnswer, ChessProblem.ChessStatus.ToString().ToLower(), "Failed test " + filename); + Assert.AreEqual(expectedAnswer, chessProblem.CalculateChessStatus().ToString().ToLower(), "Failed test " + filename); } } } \ No newline at end of file diff --git a/cs/Chess/TemporaryPieceMove.cs b/cs/Chess/TemporaryPieceMove.cs index 6dd1d80d1..0c825db99 100644 --- a/cs/Chess/TemporaryPieceMove.cs +++ b/cs/Chess/TemporaryPieceMove.cs @@ -4,6 +4,8 @@ namespace Chess { public class TemporaryPieceMove : IDisposable { + public Board Board => board; + private readonly Board board; private readonly Location from; private readonly Piece oldDestinationPiece; diff --git a/cs/ControlDigit/Snils/SnilsExtensions.cs b/cs/ControlDigit/Snils/SnilsExtensions.cs index e4dd90326..81e143f38 100644 --- a/cs/ControlDigit/Snils/SnilsExtensions.cs +++ b/cs/ControlDigit/Snils/SnilsExtensions.cs @@ -6,7 +6,25 @@ public static class SnilsExtensions { public static int CalculateSnils(this long number) { - throw new NotImplementedException(); + var resultSum = UpcExtensions.CalculateSum(number, CalculateDigitSnils); + var result = 0; + + while (resultSum > 101) + { + resultSum %= 101; + } + + if (resultSum < 100) + { + result = resultSum; + } + + return result; + } + + private static int CalculateDigitSnils(int digit, int position) + { + return digit * position; } } } diff --git a/cs/ControlDigit/Upc/UpcExtensions.cs b/cs/ControlDigit/Upc/UpcExtensions.cs index d1c376330..6beff5e9d 100644 --- a/cs/ControlDigit/Upc/UpcExtensions.cs +++ b/cs/ControlDigit/Upc/UpcExtensions.cs @@ -1,12 +1,57 @@ using System; +using System.Linq; namespace ControlDigit { public static class UpcExtensions { + private const int MULTIPLIER_ODD_POSITIONS = 3; + public static int CalculateUpc(this long number) { - throw new NotImplementedException(); + var resultSum = CalculateSum(number, CalculateDigitUpc); + var result = 0; + + if (resultSum % 10 != 0) + { + result = 10 - resultSum % 10; + } + + return result; + } + + public static int CalculateSum(long number, Func calculateDigit) + { + //var revertNumberString = RevertNumber(number); + + //return revertNumberString.Select(t => int.Parse(t.ToString())).Select((digit, i) => calculateDigit(digit, i + 1)).Sum(); + + var result = 0; + var position = 1; + while (number > 0) + { + var digit = (int)(number % 10); + result += calculateDigit(digit, position); + number /= 10; + position += 1; + } + + return result; + } + + private static string RevertNumber(long number) + { + return new string(number.ToString().Reverse().ToArray()); + } + + private static int CalculateDigitUpc(int digit, int position) + { + if (position % 2 == 1) + { + return digit * MULTIPLIER_ODD_POSITIONS; + } + + return digit; } } } From 502a80188950e1f8575e98b81ae27642fc173554 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 3 Nov 2025 17:21:57 +0500 Subject: [PATCH 02/19] Initial structure --- cs/Markdown/Markdown.csproj | 9 +++++++ cs/Markdown/Md.cs | 14 ++++++++++ cs/Markdown/Parser/IParser.cs | 6 +++++ cs/Markdown/Parser/ParserMarkdown.cs | 9 +++++++ cs/Markdown/Renderer/HtmlRenderer.cs | 40 ++++++++++++++++++++++++++++ cs/Markdown/Renderer/IRenderer.cs | 6 +++++ cs/Markdown/Token.cs | 15 +++++++++++ cs/Markdown/TypeTag.cs | 11 ++++++++ cs/clean-code.sln | 6 +++++ cs/clean-code.sln.DotSettings | 3 +++ 10 files changed, 119 insertions(+) create mode 100644 cs/Markdown/Markdown.csproj create mode 100644 cs/Markdown/Md.cs create mode 100644 cs/Markdown/Parser/IParser.cs create mode 100644 cs/Markdown/Parser/ParserMarkdown.cs create mode 100644 cs/Markdown/Renderer/HtmlRenderer.cs create mode 100644 cs/Markdown/Renderer/IRenderer.cs create mode 100644 cs/Markdown/Token.cs create mode 100644 cs/Markdown/TypeTag.cs diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj new file mode 100644 index 000000000..17b910f6d --- /dev/null +++ b/cs/Markdown/Markdown.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs new file mode 100644 index 000000000..9f6b496ec --- /dev/null +++ b/cs/Markdown/Md.cs @@ -0,0 +1,14 @@ +namespace Markdown; + +public class Md +{ + private readonly ParserMarkdown parser = new(); + private readonly HtmlRenderer renderer = new(); + + public string Render(string markdownText) + { + var tokens = parser.Parse(markdownText); + var result = renderer.Render(tokens); + return result; + } +} \ 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..1e5c9afdf --- /dev/null +++ b/cs/Markdown/Parser/IParser.cs @@ -0,0 +1,6 @@ +namespace Markdown; + +public interface IParser +{ + public IEnumerable Parse(string text); +} \ No newline at end of file diff --git a/cs/Markdown/Parser/ParserMarkdown.cs b/cs/Markdown/Parser/ParserMarkdown.cs new file mode 100644 index 000000000..663f30706 --- /dev/null +++ b/cs/Markdown/Parser/ParserMarkdown.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +public class ParserMarkdown: IParser +{ + public IEnumerable Parse(string text) + { + throw new NotImplementedException(); + } +} \ 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..4e962c27c --- /dev/null +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -0,0 +1,40 @@ +namespace Markdown; + +public class HtmlRenderer : IRenderer +{ + private readonly Dictionary tags = new() + { + { TypeTag.Header, "h1" }, + // TODO: add values + }; + + public string Render(IEnumerable tokens) + { + throw new NotImplementedException(); + } + + private string RenderTokenHeader(Token token) + { + throw new NotImplementedException(); + } + + private string RenderTokenItalic(Token token) + { + throw new NotImplementedException(); + } + + private string RenderTokenBold(Token token) + { + throw new NotImplementedException(); + } + + private string RenderTokenEscaping(Token token) + { + throw new NotImplementedException(); + } + + private string RenderCombinedToken(Token token) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/cs/Markdown/Renderer/IRenderer.cs b/cs/Markdown/Renderer/IRenderer.cs new file mode 100644 index 000000000..f585b126b --- /dev/null +++ b/cs/Markdown/Renderer/IRenderer.cs @@ -0,0 +1,6 @@ +namespace Markdown; + +public interface IRenderer +{ + public string Render(IEnumerable tokens); +} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs new file mode 100644 index 000000000..2a191ef7e --- /dev/null +++ b/cs/Markdown/Token.cs @@ -0,0 +1,15 @@ +namespace Markdown; + +public class Token +{ + private TypeTag typeTag; + private string content; + private List? children; + + public Token(TypeTag typeTag, string content, List? children = null) + { + this.typeTag = typeTag; + this.content = content; + this.children = children; + } +} \ No newline at end of file diff --git a/cs/Markdown/TypeTag.cs b/cs/Markdown/TypeTag.cs new file mode 100644 index 000000000..46f0c5d4e --- /dev/null +++ b/cs/Markdown/TypeTag.cs @@ -0,0 +1,11 @@ +namespace Markdown; + +public enum TypeTag +{ + None, + Header, + Italic, + Bold, + Escaping, + Link, +} \ No newline at end of file diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 2206d54db..3326411d0 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -9,6 +9,8 @@ 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", "{8794FD77-5E36-4990-A3CC-AB363934F2CC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,5 +29,9 @@ 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 + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8794FD77-5E36-4990-A3CC-AB363934F2CC}.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 From cb80f6373898f69886a39cc313bdeac07024b41d Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 3 Nov 2025 17:33:16 +0500 Subject: [PATCH 03/19] Revert "Classwork" This reverts commit ce5fffa63e45ba1ff3b9dc50c10bfe0105e3143d. --- cs/Chess/ChessProblem.cs | 56 ++++++++++-------------- cs/Chess/ChessProblem_Test.cs | 18 ++++---- cs/Chess/TemporaryPieceMove.cs | 2 - cs/ControlDigit/Snils/SnilsExtensions.cs | 20 +-------- cs/ControlDigit/Upc/UpcExtensions.cs | 47 +------------------- 5 files changed, 34 insertions(+), 109 deletions(-) diff --git a/cs/Chess/ChessProblem.cs b/cs/Chess/ChessProblem.cs index 719dc6b34..f0d6ab430 100644 --- a/cs/Chess/ChessProblem.cs +++ b/cs/Chess/ChessProblem.cs @@ -2,50 +2,42 @@ { public class ChessProblem { - private Board board; - public ChessStatus ChessStatus; + private static Board board; + public static ChessStatus ChessStatus; - public ChessProblem(Board newBoard) + public static void LoadFrom(string[] lines) { - board = newBoard; + board = new BoardParser().ParseBoard(lines); } // Определяет мат, шах или пат белым. - public ChessStatus CalculateChessStatus() + public static void CalculateChessStatus() { var isCheck = IsCheckForWhite(); - var whiteKingHasMoves = false; + var hasMoves = false; foreach (var locFrom in board.GetPieces(PieceColor.White)) { foreach (var locTo in board.GetPiece(locFrom).GetMoves(locFrom, board)) { - var temporaryMove = board.PerformTemporaryMove(locFrom, locTo); - + var old = board.GetPiece(locTo); + board.Set(locTo, board.GetPiece(locFrom)); + board.Set(locFrom, null); if (!IsCheckForWhite()) - whiteKingHasMoves = true; - - temporaryMove.Undo(); + hasMoves = true; + board.Set(locFrom, board.GetPiece(locTo)); + board.Set(locTo, old); } } - - if (isCheck && whiteKingHasMoves) - { - ChessStatus = ChessStatus.Check; - } - else if (isCheck && !whiteKingHasMoves) - { - ChessStatus = ChessStatus.Mate; - } - else if (!isCheck && !whiteKingHasMoves) - { - ChessStatus = ChessStatus.Stalemate; - } - else ChessStatus = ChessStatus.Ok; - return ChessStatus; + if (isCheck) + if (hasMoves) + ChessStatus = ChessStatus.Check; + else ChessStatus = ChessStatus.Mate; + else if (hasMoves) ChessStatus = ChessStatus.Ok; + else ChessStatus = ChessStatus.Stalemate; } // check — это шах - private bool IsCheckForWhite() + private static bool IsCheckForWhite() { var isCheck = false; foreach (var loc in board.GetPieces(PieceColor.Black)) @@ -54,13 +46,13 @@ private bool IsCheckForWhite() var moves = piece.GetMoves(loc, board); foreach (var destination in moves) { - if (Piece.Is(board.GetPiece(destination), PieceColor.White, PieceType.King)) - { - isCheck = true; - } + if (Piece.Is(board.GetPiece(destination), + PieceColor.White, PieceType.King)) + isCheck = true; } } - return isCheck; + if (isCheck) return true; + return false; } } } \ No newline at end of file diff --git a/cs/Chess/ChessProblem_Test.cs b/cs/Chess/ChessProblem_Test.cs index 5cd0ec328..bb3915771 100644 --- a/cs/Chess/ChessProblem_Test.cs +++ b/cs/Chess/ChessProblem_Test.cs @@ -21,15 +21,13 @@ public void RepeatedMethodCallDoNotChangeBehaviour() " ", " ", }; - - var board = new BoardParser().ParseBoard(boardLines); - var chessProblem = new ChessProblem(board); - var chessStatus = chessProblem.CalculateChessStatus(); - Assert.AreEqual(ChessStatus.Check, chessStatus); + ChessProblem.LoadFrom(boardLines); + ChessProblem.CalculateChessStatus(); + Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); // Now check that internal board modifications during the first call do not change answer - chessStatus = chessProblem.CalculateChessStatus(); - Assert.AreEqual(ChessStatus.Check, chessStatus); + ChessProblem.CalculateChessStatus(); + Assert.AreEqual(ChessStatus.Check, ChessProblem.ChessStatus); } [Test] @@ -48,10 +46,10 @@ public void AllTests() private static void TestOnFile(string filename) { var boardLines = File.ReadAllLines(filename); - var board = new BoardParser().ParseBoard(boardLines); - var chessProblem = new ChessProblem(board); + ChessProblem.LoadFrom(boardLines); var expectedAnswer = File.ReadAllText(Path.ChangeExtension(filename, ".ans")).Trim(); - Assert.AreEqual(expectedAnswer, chessProblem.CalculateChessStatus().ToString().ToLower(), "Failed test " + filename); + ChessProblem.CalculateChessStatus(); + Assert.AreEqual(expectedAnswer, ChessProblem.ChessStatus.ToString().ToLower(), "Failed test " + filename); } } } \ No newline at end of file diff --git a/cs/Chess/TemporaryPieceMove.cs b/cs/Chess/TemporaryPieceMove.cs index 0c825db99..6dd1d80d1 100644 --- a/cs/Chess/TemporaryPieceMove.cs +++ b/cs/Chess/TemporaryPieceMove.cs @@ -4,8 +4,6 @@ namespace Chess { public class TemporaryPieceMove : IDisposable { - public Board Board => board; - private readonly Board board; private readonly Location from; private readonly Piece oldDestinationPiece; diff --git a/cs/ControlDigit/Snils/SnilsExtensions.cs b/cs/ControlDigit/Snils/SnilsExtensions.cs index 81e143f38..e4dd90326 100644 --- a/cs/ControlDigit/Snils/SnilsExtensions.cs +++ b/cs/ControlDigit/Snils/SnilsExtensions.cs @@ -6,25 +6,7 @@ public static class SnilsExtensions { public static int CalculateSnils(this long number) { - var resultSum = UpcExtensions.CalculateSum(number, CalculateDigitSnils); - var result = 0; - - while (resultSum > 101) - { - resultSum %= 101; - } - - if (resultSum < 100) - { - result = resultSum; - } - - return result; - } - - private static int CalculateDigitSnils(int digit, int position) - { - return digit * position; + throw new NotImplementedException(); } } } diff --git a/cs/ControlDigit/Upc/UpcExtensions.cs b/cs/ControlDigit/Upc/UpcExtensions.cs index 6beff5e9d..d1c376330 100644 --- a/cs/ControlDigit/Upc/UpcExtensions.cs +++ b/cs/ControlDigit/Upc/UpcExtensions.cs @@ -1,57 +1,12 @@ using System; -using System.Linq; namespace ControlDigit { public static class UpcExtensions { - private const int MULTIPLIER_ODD_POSITIONS = 3; - public static int CalculateUpc(this long number) { - var resultSum = CalculateSum(number, CalculateDigitUpc); - var result = 0; - - if (resultSum % 10 != 0) - { - result = 10 - resultSum % 10; - } - - return result; - } - - public static int CalculateSum(long number, Func calculateDigit) - { - //var revertNumberString = RevertNumber(number); - - //return revertNumberString.Select(t => int.Parse(t.ToString())).Select((digit, i) => calculateDigit(digit, i + 1)).Sum(); - - var result = 0; - var position = 1; - while (number > 0) - { - var digit = (int)(number % 10); - result += calculateDigit(digit, position); - number /= 10; - position += 1; - } - - return result; - } - - private static string RevertNumber(long number) - { - return new string(number.ToString().Reverse().ToArray()); - } - - private static int CalculateDigitUpc(int digit, int position) - { - if (position % 2 == 1) - { - return digit * MULTIPLIER_ODD_POSITIONS; - } - - return digit; + throw new NotImplementedException(); } } } From 6ac0790aab59f8ff47f57d5c7a3e4059410b8bed Mon Sep 17 00:00:00 2001 From: Ruslan Date: Tue, 4 Nov 2025 15:16:37 +0500 Subject: [PATCH 04/19] Refactoring --- cs/Markdown/Md.cs | 4 ++-- cs/Markdown/Parser/{ParserMarkdown.cs => MarkdownParser.cs} | 2 +- cs/Markdown/Renderer/HtmlRenderer.cs | 4 ++-- cs/Markdown/{TypeTag.cs => TagType.cs} | 2 +- cs/Markdown/Token.cs | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) rename cs/Markdown/Parser/{ParserMarkdown.cs => MarkdownParser.cs} (78%) rename cs/Markdown/{TypeTag.cs => TagType.cs} (82%) diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 9f6b496ec..8109cb22d 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -2,8 +2,8 @@ public class Md { - private readonly ParserMarkdown parser = new(); - private readonly HtmlRenderer renderer = new(); + private readonly IParser parser = new MarkdownParser(); + private readonly IRenderer renderer = new HtmlRenderer(); public string Render(string markdownText) { diff --git a/cs/Markdown/Parser/ParserMarkdown.cs b/cs/Markdown/Parser/MarkdownParser.cs similarity index 78% rename from cs/Markdown/Parser/ParserMarkdown.cs rename to cs/Markdown/Parser/MarkdownParser.cs index 663f30706..98a4d1e9e 100644 --- a/cs/Markdown/Parser/ParserMarkdown.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -1,6 +1,6 @@ namespace Markdown; -public class ParserMarkdown: IParser +public class MarkdownParser: IParser { public IEnumerable Parse(string text) { diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs index 4e962c27c..3434ea333 100644 --- a/cs/Markdown/Renderer/HtmlRenderer.cs +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -2,9 +2,9 @@ public class HtmlRenderer : IRenderer { - private readonly Dictionary tags = new() + private readonly Dictionary tags = new() { - { TypeTag.Header, "h1" }, + { TagType.Header, "h1" }, // TODO: add values }; diff --git a/cs/Markdown/TypeTag.cs b/cs/Markdown/TagType.cs similarity index 82% rename from cs/Markdown/TypeTag.cs rename to cs/Markdown/TagType.cs index 46f0c5d4e..2c787fb49 100644 --- a/cs/Markdown/TypeTag.cs +++ b/cs/Markdown/TagType.cs @@ -1,6 +1,6 @@ namespace Markdown; -public enum TypeTag +public enum TagType { None, Header, diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index 2a191ef7e..df5e995ff 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -2,13 +2,13 @@ public class Token { - private TypeTag typeTag; + private TagType tagType; private string content; private List? children; - public Token(TypeTag typeTag, string content, List? children = null) + public Token(TagType tagType, string content, List? children = null) { - this.typeTag = typeTag; + this.tagType = tagType; this.content = content; this.children = children; } From 721819764fd6cb223f945c14e4ad77c9f4d3ac19 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sun, 9 Nov 2025 23:57:12 +0500 Subject: [PATCH 05/19] Parser --- cs/Markdown/Markdown.csproj | 6 + cs/Markdown/Parser/MarkdownParser.cs | 499 ++++++++++++++++++++++- cs/Markdown/Renderer/HtmlRenderer.cs | 7 +- cs/Markdown/Tag/Tag.cs | 8 + cs/Markdown/{ => Tag}/TagType.cs | 1 + cs/Markdown/Tests/HtmlRendererTests.cs | 9 + cs/Markdown/Tests/MarkdownParserTests.cs | 269 ++++++++++++ cs/Markdown/Token.cs | 15 +- 8 files changed, 801 insertions(+), 13 deletions(-) create mode 100644 cs/Markdown/Tag/Tag.cs rename cs/Markdown/{ => Tag}/TagType.cs (89%) create mode 100644 cs/Markdown/Tests/HtmlRendererTests.cs create mode 100644 cs/Markdown/Tests/MarkdownParserTests.cs diff --git a/cs/Markdown/Markdown.csproj b/cs/Markdown/Markdown.csproj index 17b910f6d..360133917 100644 --- a/cs/Markdown/Markdown.csproj +++ b/cs/Markdown/Markdown.csproj @@ -6,4 +6,10 @@ enable + + + + + + diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index 98a4d1e9e..f9b323b85 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -2,8 +2,505 @@ public class MarkdownParser: IParser { + private static readonly Dictionary tags = new() + { + { TagType.Header, new Tag("# ", false) }, + { TagType.Italic, new Tag("_", true) }, + { TagType.Bold, new Tag("__", true) }, + { TagType.Escaping, new Tag("\\", false, 2 ) }, + { TagType.EndOfLine, new Tag("\n", false) }, + //{ TagType.Link, "" }, + }; + + private static readonly HashSet escapeSymbols = ['\\', '#', '_']; + public IEnumerable Parse(string text) { - throw new NotImplementedException(); + var result = new List(); + var tokensWithOpenTag = new Stack(); + + OpenToken? openTokenWithEmptyTag = null; + int currentTagLength; + + for (var i = 0; i < text.Length; i += currentTagLength) + { + var currentTag = GetTagType(text, i, tokensWithOpenTag); + + if (currentTag == TagType.None) + { + currentTagLength = 1; + openTokenWithEmptyTag ??= new OpenToken(TagType.None, i); + continue; + } + + if (openTokenWithEmptyTag is not null) + { + var token = CreateToken(text, i, openTokenWithEmptyTag); + AddToken(token, tokensWithOpenTag, result); + } + + openTokenWithEmptyTag = null; + + if (tags[currentTag].IsPairedTag) + { + ProcessPairedTag(text, currentTag, tokensWithOpenTag, i, result); + } + else + { + ProcessUnpairedTag(text, currentTag, tokensWithOpenTag, i, result); + } + + currentTagLength = tags[currentTag].TotalTagLength; + } + + result = AddUnfinishedTags(text, openTokenWithEmptyTag, tokensWithOpenTag, result); + result = ProcessBorderlineCases(result); + + return result; + } + + #region Processing Tags + + private static void ProcessUnpairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, int position, + List result) + { + if (tags[currentTag].IsPairedTag) + { + throw new ArgumentException("Unpaired tag was expected, but paired tag was received"); + } + + var currentTagLength = tags[currentTag].Content.Length; + + switch (currentTag) + { + case TagType.Header: + { + var openToken = new OpenToken(currentTag, position + currentTagLength); + tokensWithOpenTag.Push(openToken); + break; + } + case TagType.EndOfLine: + ProcessTagEndOfLine(text, tokensWithOpenTag, position, result); + break; + case TagType.Escaping: + { + var startPosition = position + currentTagLength; + var openToken = new OpenToken(currentTag, startPosition); + var token = CreateToken(text, startPosition + 1, openToken); + AddToken(token, tokensWithOpenTag, result); + break; + } + } + } + + private static void ProcessTagEndOfLine(string text, Stack tokensWithOpenTag, int position, List result) + { + var isStackContainsTagHeader = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(TagType.Header); + var openTokenEndOfLine = new OpenToken(TagType.EndOfLine, position); + var tokenEndOfLine = CreateToken(text, position, openTokenEndOfLine); + + AddToken(tokenEndOfLine, tokensWithOpenTag, result); + + if (isStackContainsTagHeader) + { + while (tokensWithOpenTag.Peek().OpenTagType != TagType.Header) + { + var openToken = tokensWithOpenTag.Pop(); + var token = CreateTokenForPairedTagWithoutPair(text, text.Length, openToken); + AddToken(token, tokensWithOpenTag, result); + } + var openTokenHeader = tokensWithOpenTag.Pop(); + var tokenHeader = CreateToken(text, text.Length, openTokenHeader); + + AddToken(tokenHeader, tokensWithOpenTag, result); + } + } + + private static void ProcessPairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, int position, List result) + { + if (!tags[currentTag].IsPairedTag) + { + throw new ArgumentException("Paired tag was expected, but unpaired tag was received"); + } + + var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(currentTag); + + switch (isStackContainsCurrentTag) + { + case true when tokensWithOpenTag.Peek().OpenTagType == currentTag: + { + var openToken = tokensWithOpenTag.Pop(); + var token = CreateToken(text, position, openToken); + AddToken(token, tokensWithOpenTag, result); + break; + } + case true when tokensWithOpenTag.Peek().OpenTagType != currentTag: + break; + case false: + { + var currentTagLength = tags[currentTag].Content.Length; + var openToken = new OpenToken(currentTag, position + currentTagLength); + tokensWithOpenTag.Push(openToken); + break; + } + } + } + + #endregion + + #region BorderlineCases + + private static List ProcessBorderlineCases(List tokens) + { + var result = ProcessEmptyUnderscores(tokens); + result = ProcessBoldTagInsideItalicTag(result); + return result; + } + + /// + /// true - if the Bold or Italic tags are located inside words; + /// + private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, int endPosition, OpenToken openToken) + { + if (openToken.OpenTagType != TagType.Bold && openToken.OpenTagType != TagType.Italic) + { + return false; + } + + var tagLength = tags[openToken.OpenTagType].Content.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + + + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && !char.IsWhiteSpace(text[openToken.TextStartPosition]) && !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && !char.IsWhiteSpace(text[endPosition + tagLength]) && !char.IsWhiteSpace(text[endPosition - 1]); + var textContainsSeveralWords = false; + + var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + foreach (var symbol in part) + { + if (char.IsWhiteSpace(symbol)) + { + textContainsSeveralWords = true; + break; + } + + } + + if ((isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords) + { + return true; + } + return false; + } + + private static Token ProcessTokenWithNumbers(Token token) + { + if (token.TagType is TagType.Bold or TagType.Italic && token.Content.Any(char.IsDigit)) + { + var tagContent = tags[token.TagType].Content; + var result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}"); + return result; + } + return token; + } + + private static List ProcessEmptyUnderscores(List tokens) + { + var result = tokens.ToList(); + for (var i = 0; i < tokens.Count; i++) + { + if (result[i] is { TagType: TagType.Bold, Content.Length: 0, Children: null }) + { + result[i] = new Token(TagType.None, string.Concat(Enumerable.Repeat(tags[TagType.Bold].Content, 2))); + } + } + return result; + } + + private static List ProcessBoldTagInsideItalicTag(List tokens) + { + var result = tokens.ToList(); + for (var i = 0; i < result.Count; i++) + { + result[i] = SearchForTokensTagItalicAndConvertTokensTagBold(result[i]); + } + return result; + } + + private static Token SearchForTokensTagItalicAndConvertTokensTagBold(Token token) + { + if (token is { TagType: TagType.Italic, Children: not null }) + { + var result = new Token(TagType.Italic, token.Content, []); + foreach (var child in token.Children) + { + result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); + } + return result; + } + if (token is { Children: not null }) + { + for (var i = 0; i < token.Children.Count; i++) + { + token.Children[i] = SearchForTokensTagItalicAndConvertTokensTagBold(token.Children[i]); + } + } + + return token; + } + + private static Token ConvertTokensTagBoldToTokensTagNone(Token token) + { + Token result; + switch (token) + { + case { TagType: TagType.Bold, Children: null }: + { + var tagContent = tags[TagType.Bold].Content; + result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}"); + return result; + } + case { TagType: TagType.Bold, Children: not null }: + { + var tagContent = tags[TagType.Bold].Content; + result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}", []); + foreach (var child in token.Children) + { + result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); + } + return result; + } + case { Children: not null }: + { + result = new Token(token.TagType, token.Content, []); + foreach (var child in token.Children) + { + result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); + } + return result; + } + } + + return token; + } + + #endregion + + private static List AddUnfinishedTags(string text, OpenToken? openTokenWithEmptyTag, Stack tokensWithOpenTag, List listTokens) + { + var result = listTokens.ToList(); + + if (openTokenWithEmptyTag is not null && tokensWithOpenTag.Count == 0) + { + var token = CreateToken(text, text.Length, openTokenWithEmptyTag); + AddToken(token, tokensWithOpenTag, result); + } + + while (tokensWithOpenTag.Count > 0) + { + var openToken = tokensWithOpenTag.Pop(); + if (openToken.OpenTagType != TagType.Header) + { + var token = CreateTokenForPairedTagWithoutPair(text, text.Length, openToken); + AddToken(token, tokensWithOpenTag, result); + } + else + { + var token = CreateToken(text, text.Length, openToken); + AddToken(token, tokensWithOpenTag, result); + } + } + + return result; + } + + private static void AddToken(Token token, Stack tokensWithOpenTag, List result) + { + token = ProcessTokenWithNumbers(token); + + if (tokensWithOpenTag.Count == 0) + { + result.Add(token); + } + else + { + tokensWithOpenTag.Peek().NestedTokens.Add(token); + } + } + + private static Token CreateToken(string text, int endPosition, OpenToken openToken) + { + if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken)) + { + var tagContent = tags[openToken.OpenTagType].Content; + var content = $"{tagContent}{text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition)}{tagContent}"; + return new Token(TagType.None, content); + } + + if (openToken.NestedTokens is [{ TagType: TagType.None }] || openToken.NestedTokens.Count == 0) + { + return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition)); + } + + return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition), openToken.NestedTokens); + } + + private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosition, OpenToken openToken) + { + var startPosition = openToken.TextStartPosition - tags[openToken.OpenTagType].Content.Length; + return new Token(TagType.None, text.Substring(startPosition, endPosition - startPosition)); + } + + private static TagType GetTagType(string text, int position, Stack tokensWithOpenTag) + { + var possibleTags = new Dictionary(); + + foreach (var keyValuePair in tags) + { + var tagLength = keyValuePair.Value.Content.Length; + var tagContent = keyValuePair.Value.Content; + + if (tagLength + position > text.Length) + { + continue; + } + + if (CheckAdditionalConditionsForTag(text, position, tokensWithOpenTag, keyValuePair, possibleTags)) + { + continue; + } + + if (text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal)) + { + possibleTags.Add(keyValuePair.Key, keyValuePair.Value); + } + } + + return possibleTags.Count == 0 ? TagType.None : possibleTags.OrderByDescending(x => x.Value.Content.Length).First().Key; + } + + private static bool CheckAdditionalConditionsForTag(string text, int position, Stack tokensWithOpenTag, + KeyValuePair keyValuePair, Dictionary possibleTags) + { + switch (keyValuePair.Key) + { + case TagType.Header: + { + if (IsHeader(text, position, tokensWithOpenTag)) + { + possibleTags.Add(keyValuePair.Key, keyValuePair.Value); + } + + return true; + } + case TagType.Escaping: + { + if (IsEscaping(text, position)) + { + possibleTags.Add(keyValuePair.Key, keyValuePair.Value); + } + + return true; + } + case TagType.Bold: + { + if (IsTag(text, position, tokensWithOpenTag, TagType.Bold)) + { + possibleTags.Add(keyValuePair.Key, keyValuePair.Value); + } + + return true; + } + case TagType.Italic: + { + if (IsTag(text, position, tokensWithOpenTag, TagType.Italic)) + { + possibleTags.Add(keyValuePair.Key, keyValuePair.Value); + } + + return true; + } + } + + return false; + } + + private static bool IsHeader(string text, int position, Stack tokensWithOpenTag) + { + var tagLength = tags[TagType.Header].Content.Length; + var tagContent = tags[TagType.Header].Content; + var isNewParagraph = position == 0 || (position >= 2 && text.AsSpan(position - 2, 2).Equals("\n\n", StringComparison.Ordinal)); + var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(TagType.Header); + + return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isNewParagraph && !isStackContainsCurrentTag; + } + + /// + /// Check only for Bold and Italic tags + /// + private static bool IsTag(string text, int position, Stack tokensWithOpenTag, TagType tagType) + { + if (tagType != TagType.Bold && tagType != TagType.Italic) + { + throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); + } + + var tagLength = tags[tagType].Content.Length; + var tagContent = tags[tagType].Content; + var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(tagType); + var isSatisfiesConditions = false; + if (isStackContainsCurrentTag) + { + isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); + } + else if (text.Length > position + tagLength) + { + isSatisfiesConditions = !char.IsWhiteSpace(text[position + tagLength]); + } + else + { + isSatisfiesConditions = true; + } + + if (tagType == TagType.Italic) + { + var isContainsTagItalic = text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal); + var tagBoldLength = tags[TagType.Bold].Content.Length; + var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) + .Equals(tags[TagType.Bold].Content, StringComparison.Ordinal); + return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; + } + + return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isSatisfiesConditions; + } + + private static bool IsEscaping(string text, int position) + { + var tagLength = tags[TagType.Escaping].Content.Length; + var tagContent = tags[TagType.Escaping].Content; + var isCanEscaping = false; + + if (text.Length <= position + 1) + { + return false; + } + + foreach (var symbol in escapeSymbols) + { + if (text[position + 1] == symbol) + { + isCanEscaping = true; + } + } + + return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isCanEscaping; + } + + private class OpenToken(TagType openTagType, int textStartPosition) + { + public readonly TagType OpenTagType = openTagType; + public readonly int TextStartPosition = textStartPosition; + public readonly List NestedTokens = []; } } \ No newline at end of file diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs index 3434ea333..cdb7f010e 100644 --- a/cs/Markdown/Renderer/HtmlRenderer.cs +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -4,8 +4,11 @@ public class HtmlRenderer : IRenderer { private readonly Dictionary tags = new() { - { TagType.Header, "h1" }, - // TODO: add values + { TagType.Header, "

" }, + { TagType.Italic, "" }, + { TagType.Bold, "" }, + { TagType.Escaping, "\\" }, + { TagType.Link, "" }, }; public string Render(IEnumerable tokens) diff --git a/cs/Markdown/Tag/Tag.cs b/cs/Markdown/Tag/Tag.cs new file mode 100644 index 000000000..bb5b96ebb --- /dev/null +++ b/cs/Markdown/Tag/Tag.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public class Tag(string content, bool isPairedTag, int? tagLength = null) +{ + public string Content { get; } = content; + public bool IsPairedTag { get; } = isPairedTag; + public int TotalTagLength { get; init; } = tagLength ?? content.Length; +} \ No newline at end of file diff --git a/cs/Markdown/TagType.cs b/cs/Markdown/Tag/TagType.cs similarity index 89% rename from cs/Markdown/TagType.cs rename to cs/Markdown/Tag/TagType.cs index 2c787fb49..90309fefa 100644 --- a/cs/Markdown/TagType.cs +++ b/cs/Markdown/Tag/TagType.cs @@ -8,4 +8,5 @@ public enum TagType Bold, Escaping, Link, + EndOfLine } \ No newline at end of file diff --git a/cs/Markdown/Tests/HtmlRendererTests.cs b/cs/Markdown/Tests/HtmlRendererTests.cs new file mode 100644 index 000000000..e43af9b0f --- /dev/null +++ b/cs/Markdown/Tests/HtmlRendererTests.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; + +namespace Markdown.Tests; + +[TestFixture] +public class HtmlRendererTests +{ + +} \ No newline at end of file diff --git a/cs/Markdown/Tests/MarkdownParserTests.cs b/cs/Markdown/Tests/MarkdownParserTests.cs new file mode 100644 index 000000000..db658d2f6 --- /dev/null +++ b/cs/Markdown/Tests/MarkdownParserTests.cs @@ -0,0 +1,269 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown.Tests; + +[TestFixture] +public class MarkdownParserTests +{ + private static readonly Dictionary pairedTags = new() + { + { TagType.Italic, "_" }, + { TagType.Bold, "__" }, + }; + private IParser parser; + + [SetUp] + public void SetUp() + { + parser = new MarkdownParser(); + } + + [TestCase("wordA wordB")] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithoutTags(string text) + { + var result = parser.Parse(text); + + result.Should().BeEquivalentTo([new Token(TagType.None, "wordA wordB")]); + } + + [TestCaseSource(nameof(CasesWhenTextWithOnePairedTag))] + [Description("Checks each paired tag")] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithOnePairedTag(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNonNestedPairedTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNestedPairedTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithPairedTagWithoutPair))] + [Description("Checks each paired tag")] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithPairedTagWithoutPair(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] + public void Parse_ReturnsIEnumerableTokens_WhenEmptyTextInsideTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesTextContainsHeaderTag))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsHeaderTag(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsEscapingTag(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsOverlappingTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] + public void Parse_ReturnsIEnumerableTokens_WhenBoldTagInsideItalicTag(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] + public void Parse_ReturnsIEnumerableTokens_WhenItalicTagInsideBoldTag(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithNumbersAndContainsBoldItalicTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] + public void Parse_ReturnsIEnumerableTokens_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsBoldItalicTagsInMiddleWords(string input, IEnumerable expectedResult) + { + var result = parser.Parse(input); + + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); + } + + public static IEnumerable CasesWhenTextWithOnePairedTag() + { + foreach (var (tagType, tagContent) in pairedTags) + { + yield return new TestCaseData($"{tagContent}wordA wordB{tagContent}", new[] { new Token(tagType, "wordA wordB") }); + yield return new TestCaseData($"wordA {tagContent}wordB wordC{tagContent} wordD", + new[] + { + new Token(TagType.None, "wordA "), new Token(tagType, "wordB wordC"), + new Token(TagType.None, " wordD") + }); + } + } + + public static IEnumerable CasesWhenTextWithMultipleNonNestedPairedTags() + { + yield return new TestCaseData("_wordA_ __wordB__ wordC", + new[] + { + new Token(TagType.Italic, "wordA"), new Token(TagType.None, " "), new Token(TagType.Bold, "wordB"), + new Token(TagType.None, " wordC") + }); + } + + public static IEnumerable CasesWhenTextWithMultipleNestedPairedTags() + { + yield return new TestCaseData("__wordA _wordB_ wordC__", + new[] + { + new Token(TagType.Bold, "wordA _wordB_ wordC", + [ + new Token(TagType.None, "wordA "), new Token(TagType.Italic, "wordB"), + new Token(TagType.None, " wordC") + ]) + }); + } + + public static IEnumerable CasesWhenTextWithPairedTagWithoutPair() + { + foreach (var (tagType, tagContent) in pairedTags) + { + yield return new TestCaseData($"{tagContent}wordA", new[] { new Token(TagType.None, $"{tagContent}wordA") }); + } + } + + public static IEnumerable CasesWhenEmptyTextInsideTags() + { + yield return new TestCaseData("__", new[] { new Token(TagType.None, "__") }); + yield return new TestCaseData("____", new[] { new Token(TagType.None, "____") }); + } + + public static IEnumerable CasesTextContainsHeaderTag() + { + yield return new TestCaseData("# wordA", new[] { new Token(TagType.Header, "wordA") }); + yield return new TestCaseData("# wordA # ", new[] { new Token(TagType.Header, "wordA # ") }); + yield return new TestCaseData(" # wordA", new[] { new Token(TagType.None, " # wordA") }); + + yield return new TestCaseData("# wordA\n # ", + new[] + { + new Token(TagType.Header, "wordA\n # ", + [new Token(TagType.None, "wordA"), new Token(TagType.EndOfLine, "")]), + new Token(TagType.None, " # ") + }); + + yield return new TestCaseData(" wordA\n\n# wordB", + new[] + { + new Token(TagType.None, " wordA"), new Token(TagType.EndOfLine, ""), + new Token(TagType.EndOfLine, ""), new Token(TagType.Header, "wordB") + }); + } + + public static IEnumerable CasesWhenTextContainsEscapingTag() + { + yield return new TestCaseData(@"\\", new[] { new Token(TagType.Escaping, @"\") }); + yield return new TestCaseData(@"\_wordA_", + new[] { new Token(TagType.Escaping, "_"), new Token(TagType.None, "wordA"), new Token(TagType.None, "_") }); + } + + public static IEnumerable CasesWhenTextContainsOverlappingTags() + { + yield return new TestCaseData("__wordA_wordB__wordC_", new[] { new Token(TagType.None, "__wordA_wordB__wordC_") }); + yield return new TestCaseData("_wordA__wordB_wordC__ wordD", new[] { new Token(TagType.None, "_wordA__wordB_wordC__ wordD") }); + } + + public static IEnumerable CasesWhenBoldTagInsideItalicTag() + { + yield return new TestCaseData("_wordA__wordB__wordC_", + new[] + { + new Token(TagType.Italic, "wordA__wordB__wordC", + [ + new Token(TagType.None, "wordA"), new Token(TagType.None, "__wordB__"), + new Token(TagType.None, "wordC") + ]) + }); + } + + public static IEnumerable CasesWhenItalicTagInsideBoldTag() + { + yield return new TestCaseData("__wordA_wordB_wordC__", + new[] + { + new Token(TagType.Bold, "wordA_wordB_wordC", + [ + new Token(TagType.None, "wordA"), new Token(TagType.Italic, "wordB"), + new Token(TagType.None, "wordC") + ]) + }); + } + + public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() + { + yield return new TestCaseData("__word1 word2 word3__", new[] { new Token(TagType.None, "__word1 word2 word3__") }); + yield return new TestCaseData("_word1 word2 word3_", new[] { new Token(TagType.None, "_word1 word2 word3_") }); + } + + public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() + { + yield return new TestCaseData("_wordA _", new[] { new Token(TagType.None, "_wordA _") }); + yield return new TestCaseData("__wordA __", new[] { new Token(TagType.None, "__wordA __") }); + yield return new TestCaseData("_ wordA_", new[] { new Token(TagType.None, "_ wordA"), new Token(TagType.None, "_") }); + yield return new TestCaseData("__ wordA__", new[] { new Token(TagType.None, "__ wordA"), new Token(TagType.None, "__") }); + } + + public static IEnumerable CasesWhenTextContainsBoldItalicTagsInMiddleWords() + { + yield return new TestCaseData("_wor_dA", new[] { new Token(TagType.Italic, "wor"), new Token(TagType.None, "dA") }); + yield return new TestCaseData("__wor__dA", new[] { new Token(TagType.Bold, "wor"), new Token(TagType.None, "dA") }); + yield return new TestCaseData("_wordA wor_dB", new[] { new Token(TagType.None, "_wordA wor_"), new Token(TagType.None, "dB") }); + yield return new TestCaseData("__wordA wor__dB", new[] { new Token(TagType.None, "__wordA wor__"), new Token(TagType.None, "dB") }); + } +} \ No newline at end of file diff --git a/cs/Markdown/Token.cs b/cs/Markdown/Token.cs index df5e995ff..9f8e6cf78 100644 --- a/cs/Markdown/Token.cs +++ b/cs/Markdown/Token.cs @@ -1,15 +1,10 @@ namespace Markdown; -public class Token +public class Token(TagType tagType, string content, List? children = null) { - private TagType tagType; - private string content; - private List? children; + public TagType TagType { get; } = tagType; - public Token(TagType tagType, string content, List? children = null) - { - this.tagType = tagType; - this.content = content; - this.children = children; - } + public string Content { get; } = content; + + public List? Children { get; set; } = children; } \ No newline at end of file From 8753d40fdd6d7abbf64b1d0fb15116302c408d82 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 10 Nov 2025 00:42:01 +0500 Subject: [PATCH 06/19] HtmlRenderer --- cs/Markdown/Parser/MarkdownParser.cs | 24 +++++---- cs/Markdown/Renderer/HtmlRenderer.cs | 62 +++++++++++----------- cs/Markdown/Tag/HtmlTag.cs | 8 +++ cs/Markdown/Tag/{Tag.cs => MarkdownTag.cs} | 2 +- cs/Markdown/Tests/HtmlRendererTests.cs | 27 +++++++++- cs/Markdown/Tests/MarkdownParserTests.cs | 4 +- 6 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 cs/Markdown/Tag/HtmlTag.cs rename cs/Markdown/Tag/{Tag.cs => MarkdownTag.cs} (71%) diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index f9b323b85..d66667b96 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -2,13 +2,13 @@ public class MarkdownParser: IParser { - private static readonly Dictionary tags = new() + private static readonly Dictionary tags = new() { - { TagType.Header, new Tag("# ", false) }, - { TagType.Italic, new Tag("_", true) }, - { TagType.Bold, new Tag("__", true) }, - { TagType.Escaping, new Tag("\\", false, 2 ) }, - { TagType.EndOfLine, new Tag("\n", false) }, + { TagType.Header, new MarkdownTag("# ", false) }, + { TagType.Italic, new MarkdownTag("_", true) }, + { TagType.Bold, new MarkdownTag("__", true) }, + { TagType.Escaping, new MarkdownTag("\\", false, 2 ) }, + { TagType.EndOfLine, new MarkdownTag("\n", false) }, //{ TagType.Link, "" }, }; @@ -331,19 +331,21 @@ private static void AddToken(Token token, Stack tokensWithOpenTag, Li private static Token CreateToken(string text, int endPosition, OpenToken openToken) { + var length = endPosition - openToken.TextStartPosition; + if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken)) { var tagContent = tags[openToken.OpenTagType].Content; - var content = $"{tagContent}{text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition)}{tagContent}"; + var content = $"{tagContent}{text.Substring(openToken.TextStartPosition, length)}{tagContent}"; return new Token(TagType.None, content); } if (openToken.NestedTokens is [{ TagType: TagType.None }] || openToken.NestedTokens.Count == 0) { - return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition)); + return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length)); } - return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition), openToken.NestedTokens); + return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length), openToken.NestedTokens); } private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosition, OpenToken openToken) @@ -354,7 +356,7 @@ private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosi private static TagType GetTagType(string text, int position, Stack tokensWithOpenTag) { - var possibleTags = new Dictionary(); + var possibleTags = new Dictionary(); foreach (var keyValuePair in tags) { @@ -381,7 +383,7 @@ private static TagType GetTagType(string text, int position, Stack to } private static bool CheckAdditionalConditionsForTag(string text, int position, Stack tokensWithOpenTag, - KeyValuePair keyValuePair, Dictionary possibleTags) + KeyValuePair keyValuePair, Dictionary possibleTags) { switch (keyValuePair.Key) { diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs index cdb7f010e..483e7f300 100644 --- a/cs/Markdown/Renderer/HtmlRenderer.cs +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -1,43 +1,45 @@ -namespace Markdown; +using System.Text; + +namespace Markdown; public class HtmlRenderer : IRenderer { - private readonly Dictionary tags = new() + private readonly Dictionary tags = new() { - { TagType.Header, "

" }, - { TagType.Italic, "" }, - { TagType.Bold, "" }, - { TagType.Escaping, "\\" }, - { TagType.Link, "" }, + { TagType.None, new HtmlTag(false, "") }, + { TagType.Header, new HtmlTag(true, "

", "

") }, + { TagType.Italic, new HtmlTag(true, "", "") }, + { TagType.Bold, new HtmlTag(true, "", "") }, + { TagType.Escaping, new HtmlTag(true, "\\") }, + //{ TagType.Link, "" }, }; public string Render(IEnumerable tokens) { - throw new NotImplementedException(); - } - - private string RenderTokenHeader(Token token) - { - throw new NotImplementedException(); - } - - private string RenderTokenItalic(Token token) - { - throw new NotImplementedException(); + var stringBuilder = new StringBuilder(); + + foreach (var token in tokens) + { + stringBuilder.Append(RenderToken(token)); + } + + return stringBuilder.ToString(); } - private string RenderTokenBold(Token token) + private string RenderToken(Token token) { - throw new NotImplementedException(); - } - - private string RenderTokenEscaping(Token token) - { - throw new NotImplementedException(); - } - - private string RenderCombinedToken(Token token) - { - throw new NotImplementedException(); + if (token.Children is null) + { + var tag = tags[token.TagType]; + return tag.IsPairedTag ? $"{tag.StartTag}{token.Content}{tag.EndTag}" : $"{tag.StartTag}{token.Content}"; + } + + var stringBuilder = new StringBuilder(); + foreach (var child in token.Children) + { + stringBuilder.Append(RenderToken(child)); + } + + return stringBuilder.ToString(); } } \ No newline at end of file diff --git a/cs/Markdown/Tag/HtmlTag.cs b/cs/Markdown/Tag/HtmlTag.cs new file mode 100644 index 000000000..5aa7af6ed --- /dev/null +++ b/cs/Markdown/Tag/HtmlTag.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public class HtmlTag(bool isPairedTag, string startTag, string? endTag = null) +{ + public string StartTag { get; } = startTag; + public string? EndTag { get; } = endTag; + public bool IsPairedTag { get; } = isPairedTag; +} \ No newline at end of file diff --git a/cs/Markdown/Tag/Tag.cs b/cs/Markdown/Tag/MarkdownTag.cs similarity index 71% rename from cs/Markdown/Tag/Tag.cs rename to cs/Markdown/Tag/MarkdownTag.cs index bb5b96ebb..619362215 100644 --- a/cs/Markdown/Tag/Tag.cs +++ b/cs/Markdown/Tag/MarkdownTag.cs @@ -1,6 +1,6 @@ namespace Markdown; -public class Tag(string content, bool isPairedTag, int? tagLength = null) +public class MarkdownTag(string content, bool isPairedTag, int? tagLength = null) { public string Content { get; } = content; public bool IsPairedTag { get; } = isPairedTag; diff --git a/cs/Markdown/Tests/HtmlRendererTests.cs b/cs/Markdown/Tests/HtmlRendererTests.cs index e43af9b0f..49e120bb4 100644 --- a/cs/Markdown/Tests/HtmlRendererTests.cs +++ b/cs/Markdown/Tests/HtmlRendererTests.cs @@ -1,9 +1,34 @@ -using NUnit.Framework; +using FluentAssertions; +using NUnit.Framework; namespace Markdown.Tests; [TestFixture] public class HtmlRendererTests { + private IRenderer renderer; + [SetUp] + public void SetUp() + { + renderer = new HtmlRenderer(); + } + + [TestCaseSource(nameof(CasesWhenListWithOneToken))] + [Description("Checks each tag")] + public void Parse_ReturnsString_WhenListWithOneToken(IEnumerable input, string expectedResult) + { + var result = renderer.Render(input); + + result.Should().BeEquivalentTo(expectedResult); + } + + public static IEnumerable CasesWhenListWithOneToken() + { + yield return new TestCaseData(new List { new (TagType.None, "Human") }, "Human"); + yield return new TestCaseData(new List { new (TagType.Italic, "Human") }, "Human"); + yield return new TestCaseData(new List { new (TagType.Bold, "Human") }, "Human"); + yield return new TestCaseData(new List { new (TagType.Escaping, "\\") }, @"\\"); + yield return new TestCaseData(new List { new (TagType.Header, "Human") }, "

Human

"); + } } \ No newline at end of file diff --git a/cs/Markdown/Tests/MarkdownParserTests.cs b/cs/Markdown/Tests/MarkdownParserTests.cs index db658d2f6..aa02286cb 100644 --- a/cs/Markdown/Tests/MarkdownParserTests.cs +++ b/cs/Markdown/Tests/MarkdownParserTests.cs @@ -20,9 +20,9 @@ public void SetUp() } [TestCase("wordA wordB")] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithoutTags(string text) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithoutTags(string input) { - var result = parser.Parse(text); + var result = parser.Parse(input); result.Should().BeEquivalentTo([new Token(TagType.None, "wordA wordB")]); } From 570f1163f254ebc44e13357364c2063b8453f11d Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 10 Nov 2025 01:43:21 +0500 Subject: [PATCH 07/19] Refactoring and added mdTests --- cs/Markdown/Renderer/HtmlRenderer.cs | 5 +- cs/Markdown/Tests/MarkdownParserTests.cs | 161 +++++++++-------- cs/Markdown/Tests/MdTests.cs | 215 +++++++++++++++++++++++ 3 files changed, 310 insertions(+), 71 deletions(-) create mode 100644 cs/Markdown/Tests/MdTests.cs diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs index 483e7f300..af76b2ae8 100644 --- a/cs/Markdown/Renderer/HtmlRenderer.cs +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -11,6 +11,7 @@ public class HtmlRenderer : IRenderer { TagType.Italic, new HtmlTag(true, "", "") }, { TagType.Bold, new HtmlTag(true, "", "") }, { TagType.Escaping, new HtmlTag(true, "\\") }, + { TagType.EndOfLine, new HtmlTag(false, "\n") } //{ TagType.Link, "" }, }; @@ -28,9 +29,9 @@ public string Render(IEnumerable tokens) private string RenderToken(Token token) { + var tag = tags[token.TagType]; if (token.Children is null) { - var tag = tags[token.TagType]; return tag.IsPairedTag ? $"{tag.StartTag}{token.Content}{tag.EndTag}" : $"{tag.StartTag}{token.Content}"; } @@ -39,6 +40,8 @@ private string RenderToken(Token token) { stringBuilder.Append(RenderToken(child)); } + stringBuilder.Insert(0, tag.StartTag); + stringBuilder.Append(tag.EndTag); return stringBuilder.ToString(); } diff --git a/cs/Markdown/Tests/MarkdownParserTests.cs b/cs/Markdown/Tests/MarkdownParserTests.cs index aa02286cb..33d96446a 100644 --- a/cs/Markdown/Tests/MarkdownParserTests.cs +++ b/cs/Markdown/Tests/MarkdownParserTests.cs @@ -9,135 +9,148 @@ public class MarkdownParserTests private static readonly Dictionary pairedTags = new() { { TagType.Italic, "_" }, - { TagType.Bold, "__" }, + { TagType.Bold, "__" } }; + private IParser parser; - + [SetUp] public void SetUp() { parser = new MarkdownParser(); } - + [TestCase("wordA wordB")] public void Parse_ReturnsIEnumerableTokens_WhenTextWithoutTags(string input) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo([new Token(TagType.None, "wordA wordB")]); } - + [TestCaseSource(nameof(CasesWhenTextWithOnePairedTag))] [Description("Checks each paired tag")] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithOnePairedTag(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithOnePairedTag(string input, IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNonNestedPairedTags(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNonNestedPairedTags(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNestedPairedTags(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithMultipleNestedPairedTags(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextWithPairedTagWithoutPair))] [Description("Checks each paired tag")] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithPairedTagWithoutPair(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithPairedTagWithoutPair(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] - public void Parse_ReturnsIEnumerableTokens_WhenEmptyTextInsideTags(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenEmptyTextInsideTags(string input, IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesTextContainsHeaderTag))] - public void Parse_ReturnsIEnumerableTokens_WhenTextContainsHeaderTag(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsHeaderTag(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] - public void Parse_ReturnsIEnumerableTokens_WhenTextContainsEscapingTag(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsEscapingTag(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] - public void Parse_ReturnsIEnumerableTokens_WhenTextContainsOverlappingTags(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsOverlappingTags(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] - public void Parse_ReturnsIEnumerableTokens_WhenBoldTagInsideItalicTag(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenBoldTagInsideItalicTag(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] - public void Parse_ReturnsIEnumerableTokens_WhenItalicTagInsideBoldTag(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenItalicTagInsideBoldTag(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithNumbersAndContainsBoldItalicTags(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithNumbersAndContainsBoldItalicTags(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] - public void Parse_ReturnsIEnumerableTokens_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] - public void Parse_ReturnsIEnumerableTokens_WhenTextContainsBoldItalicTagsInMiddleWords(string input, IEnumerable expectedResult) + public void Parse_ReturnsIEnumerableTokens_WhenTextContainsBoldItalicTagsInMiddleWords(string input, + IEnumerable expectedResult) { var result = parser.Parse(input); - + result.Should().BeEquivalentTo(expectedResult, options => options.WithStrictOrdering()); } - + public static IEnumerable CasesWhenTextWithOnePairedTag() { foreach (var (tagType, tagContent) in pairedTags) { - yield return new TestCaseData($"{tagContent}wordA wordB{tagContent}", new[] { new Token(tagType, "wordA wordB") }); + yield return new TestCaseData($"{tagContent}wordA wordB{tagContent}", + new[] { new Token(tagType, "wordA wordB") }); yield return new TestCaseData($"wordA {tagContent}wordB wordC{tagContent} wordD", new[] { @@ -146,7 +159,7 @@ public static IEnumerable CasesWhenTextWithOnePairedTag() }); } } - + public static IEnumerable CasesWhenTextWithMultipleNonNestedPairedTags() { yield return new TestCaseData("_wordA_ __wordB__ wordC", @@ -156,7 +169,7 @@ public static IEnumerable CasesWhenTextWithMultipleNonNestedPaired new Token(TagType.None, " wordC") }); } - + public static IEnumerable CasesWhenTextWithMultipleNestedPairedTags() { yield return new TestCaseData("__wordA _wordB_ wordC__", @@ -169,21 +182,20 @@ public static IEnumerable CasesWhenTextWithMultipleNestedPairedTag ]) }); } - + public static IEnumerable CasesWhenTextWithPairedTagWithoutPair() { foreach (var (tagType, tagContent) in pairedTags) - { - yield return new TestCaseData($"{tagContent}wordA", new[] { new Token(TagType.None, $"{tagContent}wordA") }); - } + yield return new TestCaseData($"{tagContent}wordA", + new[] { new Token(TagType.None, $"{tagContent}wordA") }); } - + public static IEnumerable CasesWhenEmptyTextInsideTags() { yield return new TestCaseData("__", new[] { new Token(TagType.None, "__") }); yield return new TestCaseData("____", new[] { new Token(TagType.None, "____") }); } - + public static IEnumerable CasesTextContainsHeaderTag() { yield return new TestCaseData("# wordA", new[] { new Token(TagType.Header, "wordA") }); @@ -197,7 +209,7 @@ public static IEnumerable CasesTextContainsHeaderTag() [new Token(TagType.None, "wordA"), new Token(TagType.EndOfLine, "")]), new Token(TagType.None, " # ") }); - + yield return new TestCaseData(" wordA\n\n# wordB", new[] { @@ -205,20 +217,22 @@ public static IEnumerable CasesTextContainsHeaderTag() new Token(TagType.EndOfLine, ""), new Token(TagType.Header, "wordB") }); } - + public static IEnumerable CasesWhenTextContainsEscapingTag() { yield return new TestCaseData(@"\\", new[] { new Token(TagType.Escaping, @"\") }); yield return new TestCaseData(@"\_wordA_", new[] { new Token(TagType.Escaping, "_"), new Token(TagType.None, "wordA"), new Token(TagType.None, "_") }); } - + public static IEnumerable CasesWhenTextContainsOverlappingTags() { - yield return new TestCaseData("__wordA_wordB__wordC_", new[] { new Token(TagType.None, "__wordA_wordB__wordC_") }); - yield return new TestCaseData("_wordA__wordB_wordC__ wordD", new[] { new Token(TagType.None, "_wordA__wordB_wordC__ wordD") }); + yield return new TestCaseData("__wordA_wordB__wordC_", + new[] { new Token(TagType.None, "__wordA_wordB__wordC_") }); + yield return new TestCaseData("_wordA__wordB_wordC__ wordD", + new[] { new Token(TagType.None, "_wordA__wordB_wordC__ wordD") }); } - + public static IEnumerable CasesWhenBoldTagInsideItalicTag() { yield return new TestCaseData("_wordA__wordB__wordC_", @@ -231,7 +245,7 @@ public static IEnumerable CasesWhenBoldTagInsideItalicTag() ]) }); } - + public static IEnumerable CasesWhenItalicTagInsideBoldTag() { yield return new TestCaseData("__wordA_wordB_wordC__", @@ -244,26 +258,33 @@ public static IEnumerable CasesWhenItalicTagInsideBoldTag() ]) }); } - + public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() { - yield return new TestCaseData("__word1 word2 word3__", new[] { new Token(TagType.None, "__word1 word2 word3__") }); + yield return new TestCaseData("__word1 word2 word3__", + new[] { new Token(TagType.None, "__word1 word2 word3__") }); yield return new TestCaseData("_word1 word2 word3_", new[] { new Token(TagType.None, "_word1 word2 word3_") }); } - + public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() { yield return new TestCaseData("_wordA _", new[] { new Token(TagType.None, "_wordA _") }); yield return new TestCaseData("__wordA __", new[] { new Token(TagType.None, "__wordA __") }); - yield return new TestCaseData("_ wordA_", new[] { new Token(TagType.None, "_ wordA"), new Token(TagType.None, "_") }); - yield return new TestCaseData("__ wordA__", new[] { new Token(TagType.None, "__ wordA"), new Token(TagType.None, "__") }); + yield return new TestCaseData("_ wordA_", + new[] { new Token(TagType.None, "_ wordA"), new Token(TagType.None, "_") }); + yield return new TestCaseData("__ wordA__", + new[] { new Token(TagType.None, "__ wordA"), new Token(TagType.None, "__") }); } - + public static IEnumerable CasesWhenTextContainsBoldItalicTagsInMiddleWords() { - yield return new TestCaseData("_wor_dA", new[] { new Token(TagType.Italic, "wor"), new Token(TagType.None, "dA") }); - yield return new TestCaseData("__wor__dA", new[] { new Token(TagType.Bold, "wor"), new Token(TagType.None, "dA") }); - yield return new TestCaseData("_wordA wor_dB", new[] { new Token(TagType.None, "_wordA wor_"), new Token(TagType.None, "dB") }); - yield return new TestCaseData("__wordA wor__dB", new[] { new Token(TagType.None, "__wordA wor__"), new Token(TagType.None, "dB") }); + yield return new TestCaseData("_wor_dA", + new[] { new Token(TagType.Italic, "wor"), new Token(TagType.None, "dA") }); + yield return new TestCaseData("__wor__dA", + new[] { new Token(TagType.Bold, "wor"), new Token(TagType.None, "dA") }); + yield return new TestCaseData("_wordA wor_dB", + new[] { new Token(TagType.None, "_wordA wor_"), new Token(TagType.None, "dB") }); + yield return new TestCaseData("__wordA wor__dB", + new[] { new Token(TagType.None, "__wordA wor__"), new Token(TagType.None, "dB") }); } } \ No newline at end of file diff --git a/cs/Markdown/Tests/MdTests.cs b/cs/Markdown/Tests/MdTests.cs new file mode 100644 index 000000000..a62214855 --- /dev/null +++ b/cs/Markdown/Tests/MdTests.cs @@ -0,0 +1,215 @@ +using FluentAssertions; +using NUnit.Framework; + +namespace Markdown.Tests; + +[TestFixture] +public class MdTests +{ + private IParser parser; + private IRenderer renderer; + + [SetUp] + public void SetUp() + { + parser = new MarkdownParser(); + renderer = new HtmlRenderer(); + } + + [TestCase("wordA wordB")] + public void Parse_ReturnsString_WhenTextWithoutTags(string input) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be("wordA wordB"); + } + + [TestCaseSource(nameof(CasesWhenTextWithOnePairedTag))] + [Description("Checks each paired tag")] + public void Parse_ReturnsString_WhenTextWithOnePairedTag(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] + public void Parse_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] + public void Parse_ReturnsString_WhenTextWithMultipleNestedPairedTags(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithPairedTagWithoutPair))] + [Description("Checks each paired tag")] + public void Parse_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] + public void Parse_ReturnsString_WhenEmptyTextInsideTags(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesTextContainsHeaderTag))] + public void Parse_ReturnsString_WhenTextContainsHeaderTag(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] + public void Parse_ReturnsString_WhenTextContainsEscapingTag(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] + public void Parse_ReturnsString_WhenTextContainsOverlappingTags(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] + public void Parse_ReturnsString_WhenBoldTagInsideItalicTag(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] + public void Parse_ReturnsString_WhenItalicTagInsideBoldTag(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] + public void Parse_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] + public void Parse_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] + public void Parse_ReturnsString_WhenTextContainsBoldItalicTagsInMiddleWords(string input, string expectedResult) + { + var result = renderer.Render(parser.Parse(input)); + + result.Should().Be(expectedResult); + } + + public static IEnumerable CasesWhenTextWithOnePairedTag() + { + yield return new TestCaseData("_wordA wordB_", "wordA wordB"); + yield return new TestCaseData("__wordA wordB__", "wordA wordB"); + yield return new TestCaseData("wordA _wordB wordC_ wordD", "wordA wordB wordC wordD"); + yield return new TestCaseData("wordA __wordB wordC__ wordD", "wordA wordB wordC wordD"); + } + + public static IEnumerable CasesWhenTextWithMultipleNonNestedPairedTags() + { + yield return new TestCaseData("_wordA_ __wordB__ wordC", "wordA wordB wordC"); + } + + public static IEnumerable CasesWhenTextWithMultipleNestedPairedTags() + { + yield return new TestCaseData("__wordA _wordB_ wordC__", "wordA wordB wordC"); + } + + public static IEnumerable CasesWhenTextWithPairedTagWithoutPair() + { + yield return new TestCaseData("_wordA", "_wordA"); + yield return new TestCaseData("__wordA", "__wordA"); + } + + public static IEnumerable CasesWhenEmptyTextInsideTags() + { + yield return new TestCaseData("__", "__"); + yield return new TestCaseData("____", "____"); + } + + public static IEnumerable CasesTextContainsHeaderTag() + { + yield return new TestCaseData("# wordA", "

wordA

"); + yield return new TestCaseData("# wordA # ", "

wordA #

"); + yield return new TestCaseData(" # wordA", " # wordA"); + yield return new TestCaseData("# wordA\n # ", "

wordA\n

# "); + yield return new TestCaseData(" wordA\n\n# wordB", " wordA\n\n

wordB

"); + } + + public static IEnumerable CasesWhenTextContainsEscapingTag() + { + yield return new TestCaseData(@"\\", @"\\"); + yield return new TestCaseData(@"\_wordA_", @"\_wordA_"); + } + + public static IEnumerable CasesWhenTextContainsOverlappingTags() + { + yield return new TestCaseData("__wordA_wordB__wordC_", "__wordA_wordB__wordC_"); + yield return new TestCaseData("_wordA__wordB_wordC__ wordD", "_wordA__wordB_wordC__ wordD"); + } + + public static IEnumerable CasesWhenBoldTagInsideItalicTag() + { + yield return new TestCaseData("_wordA__wordB__wordC_", "wordA__wordB__wordC"); + } + + public static IEnumerable CasesWhenItalicTagInsideBoldTag() + { + yield return new TestCaseData("__wordA_wordB_wordC__", "wordAwordBwordC"); + } + + public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() + { + yield return new TestCaseData("__word1 word2 word3__", "__word1 word2 word3__"); + yield return new TestCaseData("_word1 word2 word3_", "_word1 word2 word3_"); + } + + public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() + { + yield return new TestCaseData("_wordA _", "_wordA _"); + yield return new TestCaseData("__wordA __", "__wordA __"); + yield return new TestCaseData("_ wordA_", "_ wordA_"); + yield return new TestCaseData("__ wordA__", "__ wordA__"); + } + + public static IEnumerable CasesWhenTextContainsBoldItalicTagsInMiddleWords() + { + yield return new TestCaseData("_wor_dA", "wordA"); + yield return new TestCaseData("__wor__dA", "wordA"); + yield return new TestCaseData("_wordA wor_dB", "_wordA wor_dB"); + yield return new TestCaseData("__wordA wor__dB", "__wordA wor__dB"); + } +} \ No newline at end of file From 3f4d06f8b59d81e9fe84f874b8dcb7dea9aa193e Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 10 Nov 2025 01:46:52 +0500 Subject: [PATCH 08/19] Refactoring --- cs/Markdown/Tests/MdTests.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cs/Markdown/Tests/MdTests.cs b/cs/Markdown/Tests/MdTests.cs index a62214855..d1b50afb9 100644 --- a/cs/Markdown/Tests/MdTests.cs +++ b/cs/Markdown/Tests/MdTests.cs @@ -17,7 +17,7 @@ public void SetUp() } [TestCase("wordA wordB")] - public void Parse_ReturnsString_WhenTextWithoutTags(string input) + public void Render_ReturnsString_WhenTextWithoutTags(string input) { var result = renderer.Render(parser.Parse(input)); @@ -26,7 +26,7 @@ public void Parse_ReturnsString_WhenTextWithoutTags(string input) [TestCaseSource(nameof(CasesWhenTextWithOnePairedTag))] [Description("Checks each paired tag")] - public void Parse_ReturnsString_WhenTextWithOnePairedTag(string input, string expectedResult) + public void Render_ReturnsString_WhenTextWithOnePairedTag(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -34,7 +34,7 @@ public void Parse_ReturnsString_WhenTextWithOnePairedTag(string input, string ex } [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] - public void Parse_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string input, string expectedResult) + public void Render_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -42,7 +42,7 @@ public void Parse_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string i } [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] - public void Parse_ReturnsString_WhenTextWithMultipleNestedPairedTags(string input, string expectedResult) + public void Render_ReturnsString_WhenTextWithMultipleNestedPairedTags(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -51,7 +51,7 @@ public void Parse_ReturnsString_WhenTextWithMultipleNestedPairedTags(string inpu [TestCaseSource(nameof(CasesWhenTextWithPairedTagWithoutPair))] [Description("Checks each paired tag")] - public void Parse_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, string expectedResult) + public void Render_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -59,7 +59,7 @@ public void Parse_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, s } [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] - public void Parse_ReturnsString_WhenEmptyTextInsideTags(string input, string expectedResult) + public void Render_ReturnsString_WhenEmptyTextInsideTags(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -67,7 +67,7 @@ public void Parse_ReturnsString_WhenEmptyTextInsideTags(string input, string exp } [TestCaseSource(nameof(CasesTextContainsHeaderTag))] - public void Parse_ReturnsString_WhenTextContainsHeaderTag(string input, string expectedResult) + public void Render_ReturnsString_WhenTextContainsHeaderTag(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -75,7 +75,7 @@ public void Parse_ReturnsString_WhenTextContainsHeaderTag(string input, string e } [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] - public void Parse_ReturnsString_WhenTextContainsEscapingTag(string input, string expectedResult) + public void Render_ReturnsString_WhenTextContainsEscapingTag(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -83,7 +83,7 @@ public void Parse_ReturnsString_WhenTextContainsEscapingTag(string input, string } [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] - public void Parse_ReturnsString_WhenTextContainsOverlappingTags(string input, string expectedResult) + public void Render_ReturnsString_WhenTextContainsOverlappingTags(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -91,7 +91,7 @@ public void Parse_ReturnsString_WhenTextContainsOverlappingTags(string input, st } [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] - public void Parse_ReturnsString_WhenBoldTagInsideItalicTag(string input, string expectedResult) + public void Render_ReturnsString_WhenBoldTagInsideItalicTag(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -99,7 +99,7 @@ public void Parse_ReturnsString_WhenBoldTagInsideItalicTag(string input, string } [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] - public void Parse_ReturnsString_WhenItalicTagInsideBoldTag(string input, string expectedResult) + public void Render_ReturnsString_WhenItalicTagInsideBoldTag(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -107,7 +107,7 @@ public void Parse_ReturnsString_WhenItalicTagInsideBoldTag(string input, string } [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] - public void Parse_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(string input, string expectedResult) + public void Render_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -115,7 +115,7 @@ public void Parse_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(str } [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] - public void Parse_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, string expectedResult) + public void Render_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); @@ -123,7 +123,7 @@ public void Parse_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags( } [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] - public void Parse_ReturnsString_WhenTextContainsBoldItalicTagsInMiddleWords(string input, string expectedResult) + public void Render_ReturnsString_WhenTextContainsBoldItalicTagsInMiddleWords(string input, string expectedResult) { var result = renderer.Render(parser.Parse(input)); From 1b854a4228ef6f3159661f4a2cd2f084987288f7 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 10 Nov 2025 01:50:57 +0500 Subject: [PATCH 09/19] Refactoring --- cs/Markdown/Tests/MdTests.cs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/cs/Markdown/Tests/MdTests.cs b/cs/Markdown/Tests/MdTests.cs index d1b50afb9..28f45c4aa 100644 --- a/cs/Markdown/Tests/MdTests.cs +++ b/cs/Markdown/Tests/MdTests.cs @@ -6,20 +6,18 @@ namespace Markdown.Tests; [TestFixture] public class MdTests { - private IParser parser; - private IRenderer renderer; + private Md markdown; [SetUp] public void SetUp() { - parser = new MarkdownParser(); - renderer = new HtmlRenderer(); + markdown = new Md(); } [TestCase("wordA wordB")] public void Render_ReturnsString_WhenTextWithoutTags(string input) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be("wordA wordB"); } @@ -28,7 +26,7 @@ public void Render_ReturnsString_WhenTextWithoutTags(string input) [Description("Checks each paired tag")] public void Render_ReturnsString_WhenTextWithOnePairedTag(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -36,7 +34,7 @@ public void Render_ReturnsString_WhenTextWithOnePairedTag(string input, string e [TestCaseSource(nameof(CasesWhenTextWithMultipleNonNestedPairedTags))] public void Render_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -44,7 +42,7 @@ public void Render_ReturnsString_WhenTextWithMultipleNonNestedPairedTags(string [TestCaseSource(nameof(CasesWhenTextWithMultipleNestedPairedTags))] public void Render_ReturnsString_WhenTextWithMultipleNestedPairedTags(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -53,7 +51,7 @@ public void Render_ReturnsString_WhenTextWithMultipleNestedPairedTags(string inp [Description("Checks each paired tag")] public void Render_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -61,7 +59,7 @@ public void Render_ReturnsString_WhenTextWithPairedTagWithoutPair(string input, [TestCaseSource(nameof(CasesWhenEmptyTextInsideTags))] public void Render_ReturnsString_WhenEmptyTextInsideTags(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -69,7 +67,7 @@ public void Render_ReturnsString_WhenEmptyTextInsideTags(string input, string ex [TestCaseSource(nameof(CasesTextContainsHeaderTag))] public void Render_ReturnsString_WhenTextContainsHeaderTag(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -77,7 +75,7 @@ public void Render_ReturnsString_WhenTextContainsHeaderTag(string input, string [TestCaseSource(nameof(CasesWhenTextContainsEscapingTag))] public void Render_ReturnsString_WhenTextContainsEscapingTag(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -85,7 +83,7 @@ public void Render_ReturnsString_WhenTextContainsEscapingTag(string input, strin [TestCaseSource(nameof(CasesWhenTextContainsOverlappingTags))] public void Render_ReturnsString_WhenTextContainsOverlappingTags(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -93,7 +91,7 @@ public void Render_ReturnsString_WhenTextContainsOverlappingTags(string input, s [TestCaseSource(nameof(CasesWhenBoldTagInsideItalicTag))] public void Render_ReturnsString_WhenBoldTagInsideItalicTag(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -101,7 +99,7 @@ public void Render_ReturnsString_WhenBoldTagInsideItalicTag(string input, string [TestCaseSource(nameof(CasesWhenItalicTagInsideBoldTag))] public void Render_ReturnsString_WhenItalicTagInsideBoldTag(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -109,7 +107,7 @@ public void Render_ReturnsString_WhenItalicTagInsideBoldTag(string input, string [TestCaseSource(nameof(CasesWhenTextWithNumbersAndContainsBoldItalicTags))] public void Render_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -117,7 +115,7 @@ public void Render_ReturnsString_WhenTextWithNumbersAndContainsBoldItalicTags(st [TestCaseSource(nameof(CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags))] public void Render_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } @@ -125,7 +123,7 @@ public void Render_ReturnsString_WhenTextWithWhiteSpaceAndContainsBoldItalicTags [TestCaseSource(nameof(CasesWhenTextContainsBoldItalicTagsInMiddleWords))] public void Render_ReturnsString_WhenTextContainsBoldItalicTagsInMiddleWords(string input, string expectedResult) { - var result = renderer.Render(parser.Parse(input)); + var result = markdown.Render(input); result.Should().Be(expectedResult); } From 1c576302bde59cca9c1cd120c4ef47b54d411c52 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Mon, 10 Nov 2025 03:05:57 +0500 Subject: [PATCH 10/19] Cleanup --- cs/Markdown/Parser/MarkdownParser.cs | 235 ++++++++++----------------- 1 file changed, 89 insertions(+), 146 deletions(-) diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index d66667b96..1cbbfb37b 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -1,27 +1,27 @@ namespace Markdown; -public class MarkdownParser: IParser +public class MarkdownParser : IParser { private static readonly Dictionary tags = new() { { TagType.Header, new MarkdownTag("# ", false) }, { TagType.Italic, new MarkdownTag("_", true) }, { TagType.Bold, new MarkdownTag("__", true) }, - { TagType.Escaping, new MarkdownTag("\\", false, 2 ) }, - { TagType.EndOfLine, new MarkdownTag("\n", false) }, + { TagType.Escaping, new MarkdownTag("\\", false, 2) }, + { TagType.EndOfLine, new MarkdownTag("\n", false) } //{ TagType.Link, "" }, }; - + private static readonly HashSet escapeSymbols = ['\\', '#', '_']; - + public IEnumerable Parse(string text) { var result = new List(); var tokensWithOpenTag = new Stack(); - + OpenToken? openTokenWithEmptyTag = null; int currentTagLength; - + for (var i = 0; i < text.Length; i += currentTagLength) { var currentTag = GetTagType(text, i, tokensWithOpenTag); @@ -42,35 +42,30 @@ public IEnumerable Parse(string text) openTokenWithEmptyTag = null; if (tags[currentTag].IsPairedTag) - { ProcessPairedTag(text, currentTag, tokensWithOpenTag, i, result); - } else - { ProcessUnpairedTag(text, currentTag, tokensWithOpenTag, i, result); - } - + currentTagLength = tags[currentTag].TotalTagLength; } result = AddUnfinishedTags(text, openTokenWithEmptyTag, tokensWithOpenTag, result); result = ProcessBorderlineCases(result); - + return result; } - + #region Processing Tags - private static void ProcessUnpairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, int position, + private static void ProcessUnpairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, + int position, List result) { if (tags[currentTag].IsPairedTag) - { throw new ArgumentException("Unpaired tag was expected, but paired tag was received"); - } var currentTagLength = tags[currentTag].Content.Length; - + switch (currentTag) { case TagType.Header: @@ -93,12 +88,13 @@ private static void ProcessUnpairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, int position, List result) + private static void ProcessTagEndOfLine(string text, Stack tokensWithOpenTag, int position, + List result) { var isStackContainsTagHeader = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(TagType.Header); var openTokenEndOfLine = new OpenToken(TagType.EndOfLine, position); var tokenEndOfLine = CreateToken(text, position, openTokenEndOfLine); - + AddToken(tokenEndOfLine, tokensWithOpenTag, result); if (isStackContainsTagHeader) @@ -109,6 +105,7 @@ private static void ProcessTagEndOfLine(string text, Stack tokensWith var token = CreateTokenForPairedTagWithoutPair(text, text.Length, openToken); AddToken(token, tokensWithOpenTag, result); } + var openTokenHeader = tokensWithOpenTag.Pop(); var tokenHeader = CreateToken(text, text.Length, openTokenHeader); @@ -116,15 +113,14 @@ private static void ProcessTagEndOfLine(string text, Stack tokensWith } } - private static void ProcessPairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, int position, List result) + private static void ProcessPairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, + int position, List result) { if (!tags[currentTag].IsPairedTag) - { throw new ArgumentException("Paired tag was expected, but unpaired tag was received"); - } - + var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(currentTag); - + switch (isStackContainsCurrentTag) { case true when tokensWithOpenTag.Peek().OpenTagType == currentTag: @@ -147,53 +143,49 @@ private static void ProcessPairedTag(string text, TagType currentTag, Stack ProcessBorderlineCases(List tokens) { var result = ProcessEmptyUnderscores(tokens); result = ProcessBoldTagInsideItalicTag(result); return result; } - + /// - /// true - if the Bold or Italic tags are located inside words; + /// true - if the Bold or Italic tags are located inside words; /// - private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, int endPosition, OpenToken openToken) + private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, int endPosition, + OpenToken openToken) { - if (openToken.OpenTagType != TagType.Bold && openToken.OpenTagType != TagType.Italic) - { - return false; - } + if (openToken.OpenTagType != TagType.Bold && openToken.OpenTagType != TagType.Italic) return false; var tagLength = tags[openToken.OpenTagType].Content.Length; var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; - - - var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && !char.IsWhiteSpace(text[openToken.TextStartPosition]) && !char.IsWhiteSpace(text[symbolBeforeStartTag]); - var isEndTagInMiddleWord = endPosition + tagLength < text.Length && !char.IsWhiteSpace(text[endPosition + tagLength]) && !char.IsWhiteSpace(text[endPosition - 1]); + + + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); var textContainsSeveralWords = false; - + var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); - + foreach (var symbol in part) - { if (char.IsWhiteSpace(symbol)) { textContainsSeveralWords = true; break; } - } - - if ((isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords) - { - return true; - } + if ((isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords) return true; return false; } - + private static Token ProcessTokenWithNumbers(Token token) { if (token.TagType is TagType.Bold or TagType.Italic && token.Content.Any(char.IsDigit)) @@ -202,6 +194,7 @@ private static Token ProcessTokenWithNumbers(Token token) var result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}"); return result; } + return token; } @@ -209,22 +202,16 @@ private static List ProcessEmptyUnderscores(List tokens) { var result = tokens.ToList(); for (var i = 0; i < tokens.Count; i++) - { if (result[i] is { TagType: TagType.Bold, Content.Length: 0, Children: null }) - { result[i] = new Token(TagType.None, string.Concat(Enumerable.Repeat(tags[TagType.Bold].Content, 2))); - } - } + return result; } - + private static List ProcessBoldTagInsideItalicTag(List tokens) { var result = tokens.ToList(); - for (var i = 0; i < result.Count; i++) - { - result[i] = SearchForTokensTagItalicAndConvertTokensTagBold(result[i]); - } + for (var i = 0; i < result.Count; i++) result[i] = SearchForTokensTagItalicAndConvertTokensTagBold(result[i]); return result; } @@ -233,20 +220,14 @@ private static Token SearchForTokensTagItalicAndConvertTokensTagBold(Token token if (token is { TagType: TagType.Italic, Children: not null }) { var result = new Token(TagType.Italic, token.Content, []); - foreach (var child in token.Children) - { - result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); - } + foreach (var child in token.Children) result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); return result; } + if (token is { Children: not null }) - { for (var i = 0; i < token.Children.Count; i++) - { token.Children[i] = SearchForTokensTagItalicAndConvertTokensTagBold(token.Children[i]); - } - } - + return token; } @@ -265,32 +246,27 @@ private static Token ConvertTokensTagBoldToTokensTagNone(Token token) { var tagContent = tags[TagType.Bold].Content; result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}", []); - foreach (var child in token.Children) - { - result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); - } + foreach (var child in token.Children) result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); return result; } case { Children: not null }: { result = new Token(token.TagType, token.Content, []); - foreach (var child in token.Children) - { - result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); - } + foreach (var child in token.Children) result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); return result; } } return token; } - + #endregion - private static List AddUnfinishedTags(string text, OpenToken? openTokenWithEmptyTag, Stack tokensWithOpenTag, List listTokens) + private static List AddUnfinishedTags(string text, OpenToken? openTokenWithEmptyTag, + Stack tokensWithOpenTag, List listTokens) { var result = listTokens.ToList(); - + if (openTokenWithEmptyTag is not null && tokensWithOpenTag.Count == 0) { var token = CreateToken(text, text.Length, openTokenWithEmptyTag); @@ -311,75 +287,65 @@ private static List AddUnfinishedTags(string text, OpenToken? openTokenWi AddToken(token, tokensWithOpenTag, result); } } - + return result; } private static void AddToken(Token token, Stack tokensWithOpenTag, List result) { token = ProcessTokenWithNumbers(token); - + if (tokensWithOpenTag.Count == 0) - { result.Add(token); - } else - { tokensWithOpenTag.Peek().NestedTokens.Add(token); - } } - + private static Token CreateToken(string text, int endPosition, OpenToken openToken) { var length = endPosition - openToken.TextStartPosition; - - if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken)) + + if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken)) { var tagContent = tags[openToken.OpenTagType].Content; var content = $"{tagContent}{text.Substring(openToken.TextStartPosition, length)}{tagContent}"; return new Token(TagType.None, content); } - + if (openToken.NestedTokens is [{ TagType: TagType.None }] || openToken.NestedTokens.Count == 0) - { return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length)); - } - return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length), openToken.NestedTokens); + return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length), + openToken.NestedTokens); } - + private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosition, OpenToken openToken) { var startPosition = openToken.TextStartPosition - tags[openToken.OpenTagType].Content.Length; return new Token(TagType.None, text.Substring(startPosition, endPosition - startPosition)); } - + private static TagType GetTagType(string text, int position, Stack tokensWithOpenTag) { var possibleTags = new Dictionary(); - + foreach (var keyValuePair in tags) { var tagLength = keyValuePair.Value.Content.Length; var tagContent = keyValuePair.Value.Content; - - if (tagLength + position > text.Length) - { - continue; - } + + if (tagLength + position > text.Length) continue; if (CheckAdditionalConditionsForTag(text, position, tokensWithOpenTag, keyValuePair, possibleTags)) - { continue; - } if (text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal)) - { possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - } } - return possibleTags.Count == 0 ? TagType.None : possibleTags.OrderByDescending(x => x.Value.Content.Length).First().Key; + return possibleTags.Count == 0 + ? TagType.None + : possibleTags.OrderByDescending(x => x.Value.Content.Length).First().Key; } private static bool CheckAdditionalConditionsForTag(string text, int position, Stack tokensWithOpenTag, @@ -389,37 +355,27 @@ private static bool CheckAdditionalConditionsForTag(string text, int position, S { case TagType.Header: { - if (IsHeader(text, position, tokensWithOpenTag)) - { - possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - } + if (IsHeader(text, position, tokensWithOpenTag)) possibleTags.Add(keyValuePair.Key, keyValuePair.Value); return true; } case TagType.Escaping: { - if (IsEscaping(text, position)) - { - possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - } + if (IsEscaping(text, position)) possibleTags.Add(keyValuePair.Key, keyValuePair.Value); return true; } case TagType.Bold: { - if (IsTag(text, position, tokensWithOpenTag, TagType.Bold)) - { + if (IsTag(text, position, tokensWithOpenTag, TagType.Bold)) possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - } return true; } case TagType.Italic: { - if (IsTag(text, position, tokensWithOpenTag, TagType.Italic)) - { + if (IsTag(text, position, tokensWithOpenTag, TagType.Italic)) possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - } return true; } @@ -432,38 +388,32 @@ private static bool IsHeader(string text, int position, Stack tokensW { var tagLength = tags[TagType.Header].Content.Length; var tagContent = tags[TagType.Header].Content; - var isNewParagraph = position == 0 || (position >= 2 && text.AsSpan(position - 2, 2).Equals("\n\n", StringComparison.Ordinal)); + var isNewParagraph = position == 0 || + (position >= 2 && text.AsSpan(position - 2, 2).Equals("\n\n", StringComparison.Ordinal)); var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(TagType.Header); - - return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isNewParagraph && !isStackContainsCurrentTag; + + return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isNewParagraph && + !isStackContainsCurrentTag; } - + /// - /// Check only for Bold and Italic tags + /// Check only for Bold and Italic tags /// private static bool IsTag(string text, int position, Stack tokensWithOpenTag, TagType tagType) { if (tagType != TagType.Bold && tagType != TagType.Italic) - { throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); - } - + var tagLength = tags[tagType].Content.Length; var tagContent = tags[tagType].Content; var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(tagType); var isSatisfiesConditions = false; if (isStackContainsCurrentTag) - { isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); - } else if (text.Length > position + tagLength) - { isSatisfiesConditions = !char.IsWhiteSpace(text[position + tagLength]); - } else - { isSatisfiesConditions = true; - } if (tagType == TagType.Italic) { @@ -473,36 +423,29 @@ private static bool IsTag(string text, int position, Stack tokensWith .Equals(tags[TagType.Bold].Content, StringComparison.Ordinal); return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; } - + return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isSatisfiesConditions; } - + private static bool IsEscaping(string text, int position) { var tagLength = tags[TagType.Escaping].Content.Length; var tagContent = tags[TagType.Escaping].Content; var isCanEscaping = false; - - if (text.Length <= position + 1) - { - return false; - } - + + if (text.Length <= position + 1) return false; + foreach (var symbol in escapeSymbols) - { if (text[position + 1] == symbol) - { isCanEscaping = true; - } - } - + return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isCanEscaping; } - + private class OpenToken(TagType openTagType, int textStartPosition) { public readonly TagType OpenTagType = openTagType; - public readonly int TextStartPosition = textStartPosition; + public readonly int TextStartPosition = textStartPosition; public readonly List NestedTokens = []; } } \ No newline at end of file From ebf6fa2b135629b8374f19dfd2114d3d220f5f3d Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 12 Nov 2025 19:06:20 +0500 Subject: [PATCH 11/19] Refactoring HtmlRenderer --- cs/Markdown/Md.cs | 2 +- cs/Markdown/Parser/MarkdownParser.cs | 1 - cs/Markdown/Renderer/HtmlRenderer.cs | 25 +++++++++++++------------ cs/Markdown/Renderer/IRenderer.cs | 2 +- cs/Markdown/Tests/HtmlRendererTests.cs | 14 +++++++------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cs/Markdown/Md.cs b/cs/Markdown/Md.cs index 8109cb22d..44691ea66 100644 --- a/cs/Markdown/Md.cs +++ b/cs/Markdown/Md.cs @@ -8,7 +8,7 @@ public class Md public string Render(string markdownText) { var tokens = parser.Parse(markdownText); - var result = renderer.Render(tokens); + var result = renderer.Render(tokens, markdownText); return result; } } \ No newline at end of file diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index 1cbbfb37b..818e57f58 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -164,7 +164,6 @@ private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, var tagLength = tags[openToken.OpenTagType].Content.Length; var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; - var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && !char.IsWhiteSpace(text[openToken.TextStartPosition]) && !char.IsWhiteSpace(text[symbolBeforeStartTag]); diff --git a/cs/Markdown/Renderer/HtmlRenderer.cs b/cs/Markdown/Renderer/HtmlRenderer.cs index af76b2ae8..39a817612 100644 --- a/cs/Markdown/Renderer/HtmlRenderer.cs +++ b/cs/Markdown/Renderer/HtmlRenderer.cs @@ -15,34 +15,35 @@ public class HtmlRenderer : IRenderer //{ TagType.Link, "" }, }; - public string Render(IEnumerable tokens) + public string Render(IEnumerable tokens, string text) { - var stringBuilder = new StringBuilder(); + var stringBuilder = new StringBuilder(text.Length); foreach (var token in tokens) { - stringBuilder.Append(RenderToken(token)); + RenderToken(token, stringBuilder); } return stringBuilder.ToString(); } - private string RenderToken(Token token) + private void RenderToken(Token token, StringBuilder builder) { var tag = tags[token.TagType]; if (token.Children is null) { - return tag.IsPairedTag ? $"{tag.StartTag}{token.Content}{tag.EndTag}" : $"{tag.StartTag}{token.Content}"; + var content = tag.IsPairedTag + ? $"{tag.StartTag}{token.Content}{tag.EndTag}" + : $"{tag.StartTag}{token.Content}"; + builder.Append(content); + return; } - - var stringBuilder = new StringBuilder(); + + builder.Append(tag.StartTag); foreach (var child in token.Children) { - stringBuilder.Append(RenderToken(child)); + RenderToken(child, builder); } - stringBuilder.Insert(0, tag.StartTag); - stringBuilder.Append(tag.EndTag); - - return stringBuilder.ToString(); + builder.Append(tag.EndTag); } } \ No newline at end of file diff --git a/cs/Markdown/Renderer/IRenderer.cs b/cs/Markdown/Renderer/IRenderer.cs index f585b126b..863f65d04 100644 --- a/cs/Markdown/Renderer/IRenderer.cs +++ b/cs/Markdown/Renderer/IRenderer.cs @@ -2,5 +2,5 @@ public interface IRenderer { - public string Render(IEnumerable tokens); + public string Render(IEnumerable tokens, string text); } \ No newline at end of file diff --git a/cs/Markdown/Tests/HtmlRendererTests.cs b/cs/Markdown/Tests/HtmlRendererTests.cs index 49e120bb4..37e9beb03 100644 --- a/cs/Markdown/Tests/HtmlRendererTests.cs +++ b/cs/Markdown/Tests/HtmlRendererTests.cs @@ -16,19 +16,19 @@ public void SetUp() [TestCaseSource(nameof(CasesWhenListWithOneToken))] [Description("Checks each tag")] - public void Parse_ReturnsString_WhenListWithOneToken(IEnumerable input, string expectedResult) + public void Parse_ReturnsString_WhenListWithOneToken(IEnumerable inputTokens, string inputText, string expectedResult) { - var result = renderer.Render(input); + var result = renderer.Render(inputTokens, inputText); result.Should().BeEquivalentTo(expectedResult); } public static IEnumerable CasesWhenListWithOneToken() { - yield return new TestCaseData(new List { new (TagType.None, "Human") }, "Human"); - yield return new TestCaseData(new List { new (TagType.Italic, "Human") }, "Human"); - yield return new TestCaseData(new List { new (TagType.Bold, "Human") }, "Human"); - yield return new TestCaseData(new List { new (TagType.Escaping, "\\") }, @"\\"); - yield return new TestCaseData(new List { new (TagType.Header, "Human") }, "

Human

"); + yield return new TestCaseData(new List { new (TagType.None, "Human") }, "Human", "Human"); + yield return new TestCaseData(new List { new (TagType.Italic, "Human") }, "Human", "Human"); + yield return new TestCaseData(new List { new (TagType.Bold, "Human") }, "Human", "Human"); + yield return new TestCaseData(new List { new (TagType.Escaping, "\\") }, "Human", @"\\"); + yield return new TestCaseData(new List { new (TagType.Header, "Human") }, "Human", "

Human

"); } } \ No newline at end of file From 76fc09e8539142872d7fcc563b51158941572fba Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 12 Nov 2025 21:06:36 +0500 Subject: [PATCH 12/19] Refactoring parser --- cs/Markdown/Parser/MarkdownParser.cs | 354 ++++++------------ cs/Markdown/PlatformType.cs | 8 + cs/Markdown/Tag/MarkdownTag.cs | 8 - .../Tag/MarkdownTag/BoldMarkdownTag.cs | 29 ++ .../Tag/MarkdownTag/EndOfLineMarkdownTag.cs | 13 + .../Tag/MarkdownTag/EscapingMarkdownTag.cs | 15 + .../Tag/MarkdownTag/HeaderMarkdownTag.cs | 21 ++ .../Tag/MarkdownTag/ItalicMarkdownTag.cs | 25 ++ cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs | 11 + .../Tag/MarkdownTag/NoneMarkdownTag.cs | 9 + cs/Markdown/Tests/HtmlRendererTests.cs | 24 ++ cs/Markdown/Tests/MarkdownParserTests.cs | 20 +- cs/Markdown/Tests/MdTests.cs | 7 +- 13 files changed, 280 insertions(+), 264 deletions(-) create mode 100644 cs/Markdown/PlatformType.cs delete mode 100644 cs/Markdown/Tag/MarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index 818e57f58..6224fe1bc 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -2,18 +2,17 @@ public class MarkdownParser : IParser { - private static readonly Dictionary tags = new() + private static readonly Dictionary markdownTagsByType = new() { - { TagType.Header, new MarkdownTag("# ", false) }, - { TagType.Italic, new MarkdownTag("_", true) }, - { TagType.Bold, new MarkdownTag("__", true) }, - { TagType.Escaping, new MarkdownTag("\\", false, 2) }, - { TagType.EndOfLine, new MarkdownTag("\n", false) } + { TagType.Header, new HeaderMarkdownTag() }, + { TagType.Italic, new ItalicMarkdownTag() }, + { TagType.Bold, new BoldMarkdownTag() }, + { TagType.Escaping, new EscapingMarkdownTag() }, + { TagType.EndOfLine, new EndOfLineMarkdownTag() }, + { TagType.None, new NoneMarkdownTag() } //{ TagType.Link, "" }, }; - private static readonly HashSet escapeSymbols = ['\\', '#', '_']; - public IEnumerable Parse(string text) { var result = new List(); @@ -24,49 +23,58 @@ public IEnumerable Parse(string text) for (var i = 0; i < text.Length; i += currentTagLength) { - var currentTag = GetTagType(text, i, tokensWithOpenTag); + var currentTag = GetTag(text, i, tokensWithOpenTag); - if (currentTag == TagType.None) + if (currentTag.TagType is TagType.None or TagType.EndOfLine) { currentTagLength = 1; - openTokenWithEmptyTag ??= new OpenToken(TagType.None, i); - continue; + openTokenWithEmptyTag ??= new OpenToken(currentTag, i); + if (currentTag.TagType is TagType.None) continue; } - if (openTokenWithEmptyTag is not null) + if (openTokenWithEmptyTag is not null && currentTag.TagType != TagType.EndOfLine) { var token = CreateToken(text, i, openTokenWithEmptyTag); AddToken(token, tokensWithOpenTag, result); + openTokenWithEmptyTag = null; } - openTokenWithEmptyTag = null; + if (currentTag.TagType == TagType.EndOfLine && + tokensWithOpenTag.Select(a => a.OpenTag.TagType).Contains(TagType.Header)) + openTokenWithEmptyTag = new OpenToken(markdownTagsByType[TagType.None], i); - if (tags[currentTag].IsPairedTag) + if (currentTag.IsPairedTag) ProcessPairedTag(text, currentTag, tokensWithOpenTag, i, result); else ProcessUnpairedTag(text, currentTag, tokensWithOpenTag, i, result); - currentTagLength = tags[currentTag].TotalTagLength; + currentTagLength = currentTag.TotalTagLength; } result = AddUnfinishedTags(text, openTokenWithEmptyTag, tokensWithOpenTag, result); - result = ProcessBorderlineCases(result); return result; } + + private static bool ContainsTagType(Stack tokensWithOpenTag, TagType tagType) => + tokensWithOpenTag.Any(t => t.OpenTag.TagType == tagType); + + + private static bool IsTopTagType(Stack tokensWithOpenTag, TagType tagType) => + tokensWithOpenTag.Count > 0 && tokensWithOpenTag.Peek().OpenTag.TagType == tagType; + #region Processing Tags - private static void ProcessUnpairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, - int position, - List result) + private static void ProcessUnpairedTag(string text, MarkdownTag currentTag, Stack tokensWithOpenTag, + int position, List result) { - if (tags[currentTag].IsPairedTag) + if (currentTag.IsPairedTag) throw new ArgumentException("Unpaired tag was expected, but paired tag was received"); - var currentTagLength = tags[currentTag].Content.Length; + var currentTagLength = currentTag.TagText.Length; - switch (currentTag) + switch (currentTag.TagType) { case TagType.Header: { @@ -88,57 +96,35 @@ private static void ProcessUnpairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, int position, - List result) + private static void ProcessTagEndOfLine(string text, Stack stack, int position, List result) { - var isStackContainsTagHeader = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(TagType.Header); - var openTokenEndOfLine = new OpenToken(TagType.EndOfLine, position); - var tokenEndOfLine = CreateToken(text, position, openTokenEndOfLine); + if (!ContainsTagType(stack, TagType.Header)) + return; - AddToken(tokenEndOfLine, tokensWithOpenTag, result); + while (stack.Count > 0 && !IsTopTagType(stack, TagType.Header)) + stack.Pop(); - if (isStackContainsTagHeader) + if (stack.Count > 0) { - while (tokensWithOpenTag.Peek().OpenTagType != TagType.Header) - { - var openToken = tokensWithOpenTag.Pop(); - var token = CreateTokenForPairedTagWithoutPair(text, text.Length, openToken); - AddToken(token, tokensWithOpenTag, result); - } - - var openTokenHeader = tokensWithOpenTag.Pop(); - var tokenHeader = CreateToken(text, text.Length, openTokenHeader); - - AddToken(tokenHeader, tokensWithOpenTag, result); + var headerToken = stack.Pop(); + AddToken(CreateToken(text, position, headerToken), stack, result); } } - private static void ProcessPairedTag(string text, TagType currentTag, Stack tokensWithOpenTag, + private static void ProcessPairedTag(string text, MarkdownTag currentTag, Stack tokensWithOpenTag, int position, List result) { - if (!tags[currentTag].IsPairedTag) + if (!currentTag.IsPairedTag) throw new ArgumentException("Paired tag was expected, but unpaired tag was received"); - var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(currentTag); - - switch (isStackContainsCurrentTag) + if (IsTopTagType(tokensWithOpenTag, currentTag.TagType) && tokensWithOpenTag.Peek().OpenTag.Equals(currentTag)) { - case true when tokensWithOpenTag.Peek().OpenTagType == currentTag: - { - var openToken = tokensWithOpenTag.Pop(); - var token = CreateToken(text, position, openToken); - AddToken(token, tokensWithOpenTag, result); - break; - } - case true when tokensWithOpenTag.Peek().OpenTagType != currentTag: - break; - case false: - { - var currentTagLength = tags[currentTag].Content.Length; - var openToken = new OpenToken(currentTag, position + currentTagLength); - tokensWithOpenTag.Push(openToken); - break; - } + var openToken = tokensWithOpenTag.Pop(); + AddToken(CreateToken(text, position, openToken), tokensWithOpenTag, result); + } + else if (!ContainsTagType(tokensWithOpenTag, currentTag.TagType)) + { + tokensWithOpenTag.Push(new OpenToken(currentTag, position + currentTag.TagText.Length)); } } @@ -146,22 +132,15 @@ private static void ProcessPairedTag(string text, TagType currentTag, Stack ProcessBorderlineCases(List tokens) - { - var result = ProcessEmptyUnderscores(tokens); - result = ProcessBoldTagInsideItalicTag(result); - return result; - } - /// /// true - if the Bold or Italic tags are located inside words; /// private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, int endPosition, OpenToken openToken) { - if (openToken.OpenTagType != TagType.Bold && openToken.OpenTagType != TagType.Italic) return false; + if (openToken.OpenTag.TagType != TagType.Bold && openToken.OpenTag.TagType != TagType.Italic) return false; - var tagLength = tags[openToken.OpenTagType].Content.Length; + var tagLength = openToken.OpenTag.TagText.Length; var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && @@ -181,79 +160,65 @@ private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, break; } - if ((isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords) return true; - return false; + return (isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords; } - private static Token ProcessTokenWithNumbers(Token token) + private static bool IsTokenHighlightsPartOfWordWithDigits(string text, int endPosition, OpenToken openToken) { - if (token.TagType is TagType.Bold or TagType.Italic && token.Content.Any(char.IsDigit)) - { - var tagContent = tags[token.TagType].Content; - var result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}"); - return result; - } + var tagLength = openToken.OpenTag.TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); - return token; + var isOnlyPartOfWordHighlighted = isStartTagInMiddleWord || isEndTagInMiddleWord; + var partText = text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + return isOnlyPartOfWordHighlighted && partText.All(char.IsLetterOrDigit) && partText.Any(char.IsDigit); } - private static List ProcessEmptyUnderscores(List tokens) + private static Token ProcessEmptyUnderscores(Token token) { - var result = tokens.ToList(); - for (var i = 0; i < tokens.Count; i++) - if (result[i] is { TagType: TagType.Bold, Content.Length: 0, Children: null }) - result[i] = new Token(TagType.None, string.Concat(Enumerable.Repeat(tags[TagType.Bold].Content, 2))); + if (token is { TagType: TagType.Bold, Content.Length: 0, Children: null }) + token = new Token(TagType.None, + string.Concat(Enumerable.Repeat(markdownTagsByType[TagType.Bold].TagText, 2))); - return result; - } - - private static List ProcessBoldTagInsideItalicTag(List tokens) - { - var result = tokens.ToList(); - for (var i = 0; i < result.Count; i++) result[i] = SearchForTokensTagItalicAndConvertTokensTagBold(result[i]); - return result; + return token; } - private static Token SearchForTokensTagItalicAndConvertTokensTagBold(Token token) + private static Token ConvertTagBoldInsideTagItalic(Token token) { if (token is { TagType: TagType.Italic, Children: not null }) { var result = new Token(TagType.Italic, token.Content, []); - foreach (var child in token.Children) result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); + foreach (var child in token.Children) result.Children!.Add(ConvertTagBoldToTagNone(child)); return result; } if (token is { Children: not null }) for (var i = 0; i < token.Children.Count; i++) - token.Children[i] = SearchForTokensTagItalicAndConvertTokensTagBold(token.Children[i]); + token.Children[i] = ConvertTagBoldInsideTagItalic(token.Children[i]); return token; } - private static Token ConvertTokensTagBoldToTokensTagNone(Token token) + private static Token ConvertTagBoldToTagNone(Token token) { - Token result; - switch (token) + if (token.TagType == TagType.Bold) { - case { TagType: TagType.Bold, Children: null }: - { - var tagContent = tags[TagType.Bold].Content; - result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}"); - return result; - } - case { TagType: TagType.Bold, Children: not null }: - { - var tagContent = tags[TagType.Bold].Content; - result = new Token(TagType.None, $"{tagContent}{token.Content}{tagContent}", []); - foreach (var child in token.Children) result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); - return result; - } - case { Children: not null }: - { - result = new Token(token.TagType, token.Content, []); - foreach (var child in token.Children) result.Children!.Add(ConvertTokensTagBoldToTokensTagNone(child)); - return result; - } + var tagContent = markdownTagsByType[TagType.Bold].TagText; + var newContent = $"{tagContent}{token.Content}{tagContent}"; + var children = token.Children?.Select(ConvertTagBoldToTagNone).ToList(); + return new Token(TagType.None, newContent, children); + } + + if (token.Children != null) + { + var children = token.Children.Select(ConvertTagBoldToTagNone).ToList(); + return new Token(token.TagType, token.Content, children); } return token; @@ -264,36 +229,32 @@ private static Token ConvertTokensTagBoldToTokensTagNone(Token token) private static List AddUnfinishedTags(string text, OpenToken? openTokenWithEmptyTag, Stack tokensWithOpenTag, List listTokens) { - var result = listTokens.ToList(); - if (openTokenWithEmptyTag is not null && tokensWithOpenTag.Count == 0) { var token = CreateToken(text, text.Length, openTokenWithEmptyTag); - AddToken(token, tokensWithOpenTag, result); + AddToken(token, tokensWithOpenTag, listTokens); } while (tokensWithOpenTag.Count > 0) { var openToken = tokensWithOpenTag.Pop(); - if (openToken.OpenTagType != TagType.Header) + if (openToken.OpenTag.TagType != TagType.Header) { var token = CreateTokenForPairedTagWithoutPair(text, text.Length, openToken); - AddToken(token, tokensWithOpenTag, result); + AddToken(token, tokensWithOpenTag, listTokens); } else { var token = CreateToken(text, text.Length, openToken); - AddToken(token, tokensWithOpenTag, result); + AddToken(token, tokensWithOpenTag, listTokens); } } - return result; + return listTokens; } private static void AddToken(Token token, Stack tokensWithOpenTag, List result) { - token = ProcessTokenWithNumbers(token); - if (tokensWithOpenTag.Count == 0) result.Add(token); else @@ -304,146 +265,49 @@ private static Token CreateToken(string text, int endPosition, OpenToken openTok { var length = endPosition - openToken.TextStartPosition; - if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken)) + var result = new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length), + openToken.NestedTokens); + + if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken) || + (openToken.OpenTag.TagType is TagType.Bold or TagType.Italic && + IsTokenHighlightsPartOfWordWithDigits(text, endPosition, openToken))) { - var tagContent = tags[openToken.OpenTagType].Content; + var tagContent = openToken.OpenTag.TagText; var content = $"{tagContent}{text.Substring(openToken.TextStartPosition, length)}{tagContent}"; return new Token(TagType.None, content); } if (openToken.NestedTokens is [{ TagType: TagType.None }] || openToken.NestedTokens.Count == 0) - return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length)); + result = new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length)); - return new Token(openToken.OpenTagType, text.Substring(openToken.TextStartPosition, length), - openToken.NestedTokens); + result = ProcessEmptyUnderscores(result); + result = ConvertTagBoldInsideTagItalic(result); + return result; } private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosition, OpenToken openToken) { - var startPosition = openToken.TextStartPosition - tags[openToken.OpenTagType].Content.Length; + var startPosition = openToken.TextStartPosition - openToken.OpenTag.TagText.Length; return new Token(TagType.None, text.Substring(startPosition, endPosition - startPosition)); } - private static TagType GetTagType(string text, int position, Stack tokensWithOpenTag) + private static MarkdownTag GetTag(string text, int position, Stack tokensWithOpenTag) { - var possibleTags = new Dictionary(); + var possibleTags = new List(); - foreach (var keyValuePair in tags) + foreach (var tag in markdownTagsByType.Values) { - var tagLength = keyValuePair.Value.Content.Length; - var tagContent = keyValuePair.Value.Content; - - if (tagLength + position > text.Length) continue; - - if (CheckAdditionalConditionsForTag(text, position, tokensWithOpenTag, keyValuePair, possibleTags)) - continue; - - if (text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal)) - possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - } - - return possibleTags.Count == 0 - ? TagType.None - : possibleTags.OrderByDescending(x => x.Value.Content.Length).First().Key; - } - - private static bool CheckAdditionalConditionsForTag(string text, int position, Stack tokensWithOpenTag, - KeyValuePair keyValuePair, Dictionary possibleTags) - { - switch (keyValuePair.Key) - { - case TagType.Header: - { - if (IsHeader(text, position, tokensWithOpenTag)) possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - - return true; - } - case TagType.Escaping: - { - if (IsEscaping(text, position)) possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - - return true; - } - case TagType.Bold: - { - if (IsTag(text, position, tokensWithOpenTag, TagType.Bold)) - possibleTags.Add(keyValuePair.Key, keyValuePair.Value); - - return true; - } - case TagType.Italic: - { - if (IsTag(text, position, tokensWithOpenTag, TagType.Italic)) - possibleTags.Add(keyValuePair.Key, keyValuePair.Value); + if (tag.TagText.Length + position > text.Length) continue; - return true; - } + if (tag.IsTag(text, position, ContainsTagType(tokensWithOpenTag, tag.TagType))) possibleTags.Add(tag); } - return false; - } - - private static bool IsHeader(string text, int position, Stack tokensWithOpenTag) - { - var tagLength = tags[TagType.Header].Content.Length; - var tagContent = tags[TagType.Header].Content; - var isNewParagraph = position == 0 || - (position >= 2 && text.AsSpan(position - 2, 2).Equals("\n\n", StringComparison.Ordinal)); - var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(TagType.Header); - - return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isNewParagraph && - !isStackContainsCurrentTag; - } - - /// - /// Check only for Bold and Italic tags - /// - private static bool IsTag(string text, int position, Stack tokensWithOpenTag, TagType tagType) - { - if (tagType != TagType.Bold && tagType != TagType.Italic) - throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); - - var tagLength = tags[tagType].Content.Length; - var tagContent = tags[tagType].Content; - var isStackContainsCurrentTag = tokensWithOpenTag.Select(a => a.OpenTagType).Contains(tagType); - var isSatisfiesConditions = false; - if (isStackContainsCurrentTag) - isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); - else if (text.Length > position + tagLength) - isSatisfiesConditions = !char.IsWhiteSpace(text[position + tagLength]); - else - isSatisfiesConditions = true; - - if (tagType == TagType.Italic) - { - var isContainsTagItalic = text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal); - var tagBoldLength = tags[TagType.Bold].Content.Length; - var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) - .Equals(tags[TagType.Bold].Content, StringComparison.Ordinal); - return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; - } - - return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isSatisfiesConditions; - } - - private static bool IsEscaping(string text, int position) - { - var tagLength = tags[TagType.Escaping].Content.Length; - var tagContent = tags[TagType.Escaping].Content; - var isCanEscaping = false; - - if (text.Length <= position + 1) return false; - - foreach (var symbol in escapeSymbols) - if (text[position + 1] == symbol) - isCanEscaping = true; - - return text.AsSpan(position, tagLength).Equals(tagContent, StringComparison.Ordinal) && isCanEscaping; + return possibleTags.OrderByDescending(x => x.TagText.Length).First(); } - private class OpenToken(TagType openTagType, int textStartPosition) + private class OpenToken(MarkdownTag openTag, int textStartPosition) { - public readonly TagType OpenTagType = openTagType; + public readonly MarkdownTag OpenTag = openTag; public readonly int TextStartPosition = textStartPosition; public readonly List NestedTokens = []; } diff --git a/cs/Markdown/PlatformType.cs b/cs/Markdown/PlatformType.cs new file mode 100644 index 000000000..df2b59202 --- /dev/null +++ b/cs/Markdown/PlatformType.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public enum PlatformType +{ + Unknown, + Windows, + Unix, +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag.cs deleted file mode 100644 index 619362215..000000000 --- a/cs/Markdown/Tag/MarkdownTag.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Markdown; - -public class MarkdownTag(string content, bool isPairedTag, int? tagLength = null) -{ - public string Content { get; } = content; - public bool IsPairedTag { get; } = isPairedTag; - public int TotalTagLength { get; init; } = tagLength ?? content.Length; -} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs new file mode 100644 index 000000000..203ee747e --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs @@ -0,0 +1,29 @@ +namespace Markdown; + +public class BoldMarkdownTag() : MarkdownTag(TagType.Bold, "__", true) +{ + public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TagType != TagType.Bold && TagType != TagType.Italic) + throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); + + var isSatisfiesConditions = false; + if (isSameTagAlreadyOpen) + isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); + else if (text.Length > position + TagText.Length) + isSatisfiesConditions = !char.IsWhiteSpace(text[position + TagText.Length]); + else + isSatisfiesConditions = true; + + // if (TagType == TagType.Italic) + // { + // var isContainsTagItalic = text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal); + // var tagBoldLength = markdownTags[TagType.Bold].TagText.Length; + // var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) + // .Equals(markdownTags[TagType.Bold].TagText, StringComparison.Ordinal); + // return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; + // } + + return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isSatisfiesConditions; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs new file mode 100644 index 000000000..6e264b971 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs @@ -0,0 +1,13 @@ +namespace Markdown; + +public class EndOfLineMarkdownTag() : MarkdownTag(TagType.EndOfLine, "\n", false) +{ + public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal)) + { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs new file mode 100644 index 000000000..c3a7e0e02 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs @@ -0,0 +1,15 @@ +namespace Markdown; + +public class EscapingMarkdownTag() : MarkdownTag(TagType.Escaping, "\\", false, 2) +{ + private static readonly List escapeSymbols = ['\\', '#', '_']; + + public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (text.Length <= position + 1) return false; + + var isCanEscaping = escapeSymbols.Contains(text[position + 1]); + + return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isCanEscaping; + } +} diff --git a/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs new file mode 100644 index 000000000..dbd51e0d3 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs @@ -0,0 +1,21 @@ +namespace Markdown; + +public class HeaderMarkdownTag() : MarkdownTag(TagType.Header, "# ", false) +{ + public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + { + var doubleNewLineUnix = "\n\n"; + var doubleNewLineWindows = "\r\n\r\n"; + + var isNewParagraphWindows = position == 0 || (position >= 4 && text.AsSpan(position - 4, 4) + .Equals(doubleNewLineWindows, StringComparison.Ordinal)); + + var isNewParagraphUnix = position == 0 || (position >= 2 && text.AsSpan(position - 2, 2) + .Equals(doubleNewLineUnix, StringComparison.Ordinal)); + + var isNewParagraph = isNewParagraphWindows || isNewParagraphUnix; + + return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isNewParagraph && + !isSameTagAlreadyOpen; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs new file mode 100644 index 000000000..daa46d170 --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs @@ -0,0 +1,25 @@ +namespace Markdown; + +public class ItalicMarkdownTag() : MarkdownTag(TagType.Italic, "_", true) +{ + public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + { + if (TagType != TagType.Bold && TagType != TagType.Italic) + throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); + + var isSatisfiesConditions = false; + if (isSameTagAlreadyOpen) + isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); + else if (text.Length > position + TagText.Length) + isSatisfiesConditions = !char.IsWhiteSpace(text[position + TagText.Length]); + else + isSatisfiesConditions = true; + + var isContainsTagItalic = text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal); + var tagBoldLength = new BoldMarkdownTag().TagText.Length; + var tagBoldText = new BoldMarkdownTag().TagText; + var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) + .Equals(tagBoldText, StringComparison.Ordinal); + return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs new file mode 100644 index 000000000..9f514c62b --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs @@ -0,0 +1,11 @@ +namespace Markdown; + +public abstract class MarkdownTag(TagType tagType, string tagText, bool isPairedTag, int? tagLength = null) +{ + public TagType TagType { get; } = tagType; + public virtual string TagText { get; } = tagText; + public virtual bool IsPairedTag { get; } = isPairedTag; + public virtual int TotalTagLength { get; init; } = tagLength ?? tagText.Length; + + public abstract bool IsTag(string text, int position, bool isSameTagAlreadyOpen); +} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs new file mode 100644 index 000000000..3ffa86a9b --- /dev/null +++ b/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +public class NoneMarkdownTag() : MarkdownTag(TagType.None, "", false) +{ + public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + { + return true; + } +} \ No newline at end of file diff --git a/cs/Markdown/Tests/HtmlRendererTests.cs b/cs/Markdown/Tests/HtmlRendererTests.cs index 37e9beb03..d86cca1c4 100644 --- a/cs/Markdown/Tests/HtmlRendererTests.cs +++ b/cs/Markdown/Tests/HtmlRendererTests.cs @@ -23,6 +23,14 @@ public void Parse_ReturnsString_WhenListWithOneToken(IEnumerable inputTok result.Should().BeEquivalentTo(expectedResult); } + [TestCaseSource(nameof(CasesWhenListWithMultipleNestedTokens))] + public void Parse_ReturnsString_WhenListWithMultipleNestedTokens(IEnumerable inputTokens, string inputText, string expectedResult) + { + var result = renderer.Render(inputTokens, inputText); + + result.Should().BeEquivalentTo(expectedResult); + } + public static IEnumerable CasesWhenListWithOneToken() { yield return new TestCaseData(new List { new (TagType.None, "Human") }, "Human", "Human"); @@ -31,4 +39,20 @@ public static IEnumerable CasesWhenListWithOneToken() yield return new TestCaseData(new List { new (TagType.Escaping, "\\") }, "Human", @"\\"); yield return new TestCaseData(new List { new (TagType.Header, "Human") }, "Human", "

Human

"); } + + public static IEnumerable CasesWhenListWithMultipleNestedTokens() + { + yield return new TestCaseData(new List { new (TagType.Header, "Human1 Human2", [ + new Token(TagType.Bold, "Human1"), + new Token(TagType.None, " "), + new Token(TagType.Italic, "Human2") + ]) }, "Human1 Human2", "

Human1 Human2

"); + + yield return new TestCaseData(new List { new (TagType.Header, "Human1 Human2", new List() + { + new (TagType.Bold, "Human1"), + new (TagType.None, " "), + new (TagType.Italic, "Human2"), + }) }, "Human1 Human2", "

Human1 Human2

"); + } } \ No newline at end of file diff --git a/cs/Markdown/Tests/MarkdownParserTests.cs b/cs/Markdown/Tests/MarkdownParserTests.cs index 33d96446a..011739965 100644 --- a/cs/Markdown/Tests/MarkdownParserTests.cs +++ b/cs/Markdown/Tests/MarkdownParserTests.cs @@ -205,16 +205,19 @@ public static IEnumerable CasesTextContainsHeaderTag() yield return new TestCaseData("# wordA\n # ", new[] { - new Token(TagType.Header, "wordA\n # ", - [new Token(TagType.None, "wordA"), new Token(TagType.EndOfLine, "")]), - new Token(TagType.None, " # ") + new Token(TagType.Header, "wordA"), + new Token(TagType.None, "\n # ") }); yield return new TestCaseData(" wordA\n\n# wordB", new[] { - new Token(TagType.None, " wordA"), new Token(TagType.EndOfLine, ""), - new Token(TagType.EndOfLine, ""), new Token(TagType.Header, "wordB") + new Token(TagType.None, " wordA\n\n"), new Token(TagType.Header, "wordB") + }); + yield return new TestCaseData(" wordA\r\n\r\n# wordB", + new[] + { + new Token(TagType.None, " wordA\r\n\r\n"), new Token(TagType.Header, "wordB") }); } @@ -261,9 +264,10 @@ public static IEnumerable CasesWhenItalicTagInsideBoldTag() public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() { - yield return new TestCaseData("__word1 word2 word3__", - new[] { new Token(TagType.None, "__word1 word2 word3__") }); - yield return new TestCaseData("_word1 word2 word3_", new[] { new Token(TagType.None, "_word1 word2 word3_") }); + yield return new TestCaseData("wo__rd1__", + new[] { new Token(TagType.None, "wo"), new Token(TagType.None, "__rd1__") }); + yield return new TestCaseData("wo_rd1_", + new[] { new Token(TagType.None, "wo"), new Token(TagType.None, "_rd1_") }); } public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() diff --git a/cs/Markdown/Tests/MdTests.cs b/cs/Markdown/Tests/MdTests.cs index 28f45c4aa..a712f97fb 100644 --- a/cs/Markdown/Tests/MdTests.cs +++ b/cs/Markdown/Tests/MdTests.cs @@ -163,8 +163,9 @@ public static IEnumerable CasesTextContainsHeaderTag() yield return new TestCaseData("# wordA", "

wordA

"); yield return new TestCaseData("# wordA # ", "

wordA #

"); yield return new TestCaseData(" # wordA", " # wordA"); - yield return new TestCaseData("# wordA\n # ", "

wordA\n

# "); + yield return new TestCaseData("# wordA\n # ", "

wordA

\n # "); yield return new TestCaseData(" wordA\n\n# wordB", " wordA\n\n

wordB

"); + yield return new TestCaseData(" wordA\r\n\r\n# wordB", " wordA\r\n\r\n

wordB

"); } public static IEnumerable CasesWhenTextContainsEscapingTag() @@ -191,8 +192,8 @@ public static IEnumerable CasesWhenItalicTagInsideBoldTag() public static IEnumerable CasesWhenTextWithNumbersAndContainsBoldItalicTags() { - yield return new TestCaseData("__word1 word2 word3__", "__word1 word2 word3__"); - yield return new TestCaseData("_word1 word2 word3_", "_word1 word2 word3_"); + yield return new TestCaseData("wo__rd1__", "wo__rd1__"); + yield return new TestCaseData("wo_rd1_", "wo_rd1_"); } public static IEnumerable CasesWhenTextWithWhiteSpaceAndContainsBoldItalicTags() From ae8c4bfc2a66c3cb19c87da7fce03a35946c39c9 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Thu, 13 Nov 2025 02:34:57 +0500 Subject: [PATCH 13/19] Refactoring --- cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs index 203ee747e..a16044a5a 100644 --- a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs @@ -15,15 +15,6 @@ public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) else isSatisfiesConditions = true; - // if (TagType == TagType.Italic) - // { - // var isContainsTagItalic = text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal); - // var tagBoldLength = markdownTags[TagType.Bold].TagText.Length; - // var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) - // .Equals(markdownTags[TagType.Bold].TagText, StringComparison.Ordinal); - // return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; - // } - return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isSatisfiesConditions; } } \ No newline at end of file From e2fd38a66c497925e8a37e7f71a557c5b31fedf9 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sat, 15 Nov 2025 18:36:50 +0500 Subject: [PATCH 14/19] Performance test --- ...n.MarkdownParserBenchmark-report-github.md | 16 ++++++ ...arkdown.MarkdownParserBenchmark-report.csv | 5 ++ ...rkdown.MarkdownParserBenchmark-report.html | 33 ++++++++++++ ...hmarkMarkdown.MdBenchmark-report-github.md | 16 ++++++ .../BenchmarkMarkdown.MdBenchmark-report.csv | 5 ++ .../BenchmarkMarkdown.MdBenchmark-report.html | 33 ++++++++++++ cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj | 25 +++++++++ cs/BenchmarkMarkdown/InputData | 54 +++++++++++++++++++ .../MarkdownParserBenchmark.cs | 31 +++++++++++ cs/BenchmarkMarkdown/MdBenchmark.cs | 31 +++++++++++ cs/BenchmarkMarkdown/Program.cs | 5 ++ cs/Markdown/Parser/MarkdownParser.cs | 6 ++- cs/Markdown/PlatformType.cs | 8 --- .../Tag/MarkdownTag/BoldMarkdownTag.cs | 3 -- .../Tag/MarkdownTag/ItalicMarkdownTag.cs | 3 -- cs/clean-code.sln | 6 +++ 16 files changed, 264 insertions(+), 16 deletions(-) create mode 100644 cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md create mode 100644 cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv create mode 100644 cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html create mode 100644 cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md create mode 100644 cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv create mode 100644 cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html create mode 100644 cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj create mode 100644 cs/BenchmarkMarkdown/InputData create mode 100644 cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs create mode 100644 cs/BenchmarkMarkdown/MdBenchmark.cs create mode 100644 cs/BenchmarkMarkdown/Program.cs delete mode 100644 cs/Markdown/PlatformType.cs diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md new file mode 100644 index 000000000..1060d0b7f --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report-github.md @@ -0,0 +1,16 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update) +AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK 9.0.306 + [Host] : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + + +``` +| Method | TextSizeMultiplier | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------- |------------------- |-------------:|------------:|------------:|------------:|-----------:|----------:|--------------:| +| **Parse** | **1** | **294.4 μs** | **5.65 μs** | **5.55 μs** | **124.5117** | **2.4414** | **-** | **1017.81 KB** | +| **Parse** | **10** | **3,172.7 μs** | **10.81 μs** | **9.59 μs** | **1363.2813** | **187.5000** | **11.7188** | **11146.04 KB** | +| **Parse** | **100** | **36,115.6 μs** | **120.37 μs** | **100.52 μs** | **12333.3333** | **333.3333** | **133.3333** | **101203 KB** | +| **Parse** | **1000** | **335,972.8 μs** | **6,497.19 μs** | **7,482.17 μs** | **127000.0000** | **33000.0000** | **4000.0000** | **1011847.79 KB** | diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv new file mode 100644 index 000000000..291e26d98 --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.csv @@ -0,0 +1,5 @@ +Method;Job;AnalyzeLaunchVariance;EvaluateOverhead;MaxAbsoluteError;MaxRelativeError;MinInvokeCount;MinIterationTime;OutlierMode;Affinity;EnvironmentVariables;Jit;LargeAddressAware;Platform;PowerPlanMode;Runtime;AllowVeryLargeObjects;Concurrent;CpuGroups;Force;HeapAffinitizeMask;HeapCount;NoAffinitize;RetainVm;Server;Arguments;BuildConfiguration;Clock;EngineFactory;NuGetReferences;Toolchain;IsMutator;InvocationCount;IterationCount;IterationTime;LaunchCount;MaxIterationCount;MaxWarmupIterationCount;MemoryRandomization;MinIterationCount;MinWarmupIterationCount;RunStrategy;UnrollFactor;WarmupCount;TextSizeMultiplier;Mean;Error;StdDev;Gen0;Gen1;Gen2;Allocated +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1;294.4 μs;5.65 μs;5.55 μs;124.5117;2.4414;0.0000;1017.81 KB +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;10;"3,172.7 μs";10.81 μs;9.59 μs;1363.2813;187.5000;11.7188;11146.04 KB +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;100;"36,115.6 μs";120.37 μs;100.52 μs;12333.3333;333.3333;133.3333;101203 KB +Parse;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1000;"335,972.8 μs";"6,497.19 μs";"7,482.17 μs";127000.0000;33000.0000;4000.0000;1011847.79 KB diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html new file mode 100644 index 000000000..53e5791ab --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MarkdownParserBenchmark-report.html @@ -0,0 +1,33 @@ + + + + +BenchmarkMarkdown.MarkdownParserBenchmark-20251115-183056 + + + + +

+BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update)
+AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 9.0.306
+  [Host]     : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+  DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+
+
+ + + + + + + + +
MethodTextSizeMultiplierMean ErrorStdDevGen0 Gen1Gen2Allocated
Parse1294.4 μs5.65 μs5.55 μs124.51172.4414-1017.81 KB
Parse103,172.7 μs10.81 μs9.59 μs1363.2813187.500011.718811146.04 KB
Parse10036,115.6 μs120.37 μs100.52 μs12333.3333333.3333133.3333101203 KB
Parse1000335,972.8 μs6,497.19 μs7,482.17 μs127000.000033000.00004000.00001011847.79 KB
+ + diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md new file mode 100644 index 000000000..8cb740937 --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md @@ -0,0 +1,16 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update) +AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK 9.0.306 + [Host] : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3 + + +``` +| Method | TextSizeMultiplier | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------- |------------------- |-------------:|------------:|-------------:|------------:|-----------:|----------:|-----------:| +| **Render** | **1** | **308.1 μs** | **4.70 μs** | **3.92 μs** | **125.4883** | **2.9297** | **-** | **1 MB** | +| **Render** | **10** | **2,951.1 μs** | **25.49 μs** | **22.59 μs** | **1246.0938** | **191.4063** | **3.9063** | **9.96 MB** | +| **Render** | **100** | **33,804.5 μs** | **547.76 μs** | **512.37 μs** | **13666.6667** | **400.0000** | **200.0000** | **109.4 MB** | +| **Render** | **1000** | **351,464.0 μs** | **6,917.49 μs** | **11,557.58 μs** | **140000.0000** | **36000.0000** | **4000.0000** | **1093.79 MB** | diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv new file mode 100644 index 000000000..7973c1d5f --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv @@ -0,0 +1,5 @@ +Method;Job;AnalyzeLaunchVariance;EvaluateOverhead;MaxAbsoluteError;MaxRelativeError;MinInvokeCount;MinIterationTime;OutlierMode;Affinity;EnvironmentVariables;Jit;LargeAddressAware;Platform;PowerPlanMode;Runtime;AllowVeryLargeObjects;Concurrent;CpuGroups;Force;HeapAffinitizeMask;HeapCount;NoAffinitize;RetainVm;Server;Arguments;BuildConfiguration;Clock;EngineFactory;NuGetReferences;Toolchain;IsMutator;InvocationCount;IterationCount;IterationTime;LaunchCount;MaxIterationCount;MaxWarmupIterationCount;MemoryRandomization;MinIterationCount;MinWarmupIterationCount;RunStrategy;UnrollFactor;WarmupCount;TextSizeMultiplier;Mean;Error;StdDev;Gen0;Gen1;Gen2;Allocated +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1;308.1 μs;4.70 μs;3.92 μs;125.4883;2.9297;0.0000;1 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;10;"2,951.1 μs";25.49 μs;22.59 μs;1246.0938;191.4063;3.9063;9.96 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;100;"33,804.5 μs";547.76 μs;512.37 μs;13666.6667;400.0000;200.0000;109.4 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1000;"351,464.0 μs";"6,917.49 μs";"11,557.58 μs";140000.0000;36000.0000;4000.0000;1093.79 MB diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html new file mode 100644 index 000000000..22df66e7a --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html @@ -0,0 +1,33 @@ + + + + +BenchmarkMarkdown.MdBenchmark-20251115-183217 + + + + +

+BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6216/22H2/2022Update)
+AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores
+.NET SDK 9.0.306
+  [Host]     : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+  DefaultJob : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
+
+
+ + + + + + + + +
MethodTextSizeMultiplierMean ErrorStdDevGen0 Gen1Gen2Allocated
Render1308.1 μs4.70 μs3.92 μs125.48832.9297-1 MB
Render102,951.1 μs25.49 μs22.59 μs1246.0938191.40633.90639.96 MB
Render10033,804.5 μs547.76 μs512.37 μs13666.6667400.0000200.0000109.4 MB
Render1000351,464.0 μs6,917.49 μs11,557.58 μs140000.000036000.00004000.00001093.79 MB
+ + diff --git a/cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj b/cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj new file mode 100644 index 000000000..2fda6a3da --- /dev/null +++ b/cs/BenchmarkMarkdown/BenchmarkMarkdown.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/cs/BenchmarkMarkdown/InputData b/cs/BenchmarkMarkdown/InputData new file mode 100644 index 000000000..876d3871f --- /dev/null +++ b/cs/BenchmarkMarkdown/InputData @@ -0,0 +1,54 @@ +# Introduction to Markdown + +Markdown is a lightweight markup language __created__ by John Gruber, with contributions from Aaron Swartz. It’s designed to be easy to read and write, using plain text conventions that translate to valid HTML. + +Markdown allows you to write __bold__ and _italic_ text, create # headings, lists, links, and more — all while keeping the text legible in a raw form. + +--- + +## Basic Formatting + +Here is an example of __bold text__ and _italic text_ in Markdown. Double underscores surround bold text, while single underscores can be used for italics. Like this: + +__This is bold__, _this is italic_, and __this is __bold with some inner _italic_ text__ too__. + +--- + +## Combining Tags + +You can combine __bold__ and _italic_ like this: ___bold italic___ text. + +# Headers + +Markdown supports headers starting with `#`: + +# Heading level 1 + +## Heading level 2 + +### Heading level 3 + +#### Heading level 4 + +--- + +## Paragraphs and line breaks + +Markdown treats one or more empty lines as paragraph breaks. + +Use two spaces at the end of a line for a line break. +Example here with a break. + +___ + +## Lists + +- __Bold item__ +- _Italic item_ +- Combined __bold and _italic_ item__ + +1. First numbered item +2. Second item with _italic_ +3. Third item with __bold__ + +--- diff --git a/cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs b/cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs new file mode 100644 index 000000000..782f6493b --- /dev/null +++ b/cs/BenchmarkMarkdown/MarkdownParserBenchmark.cs @@ -0,0 +1,31 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Markdown; + +namespace BenchmarkMarkdown; + +[MemoryDiagnoser] +public class MarkdownParserBenchmark +{ + private IParser parser; + private string fileContent; + + [Params(1, 10, 100, 1000)] public int TextSizeMultiplier { get; set; } + + [GlobalSetup] + public void Setup() + { + parser = new MarkdownParser(); + + var baseDir = AppContext.BaseDirectory; + var filePath = Path.Combine(baseDir, "InputData"); + var baseFileContent = File.ReadAllText(filePath, Encoding.UTF8); + fileContent = string.Concat(Enumerable.Repeat(baseFileContent, TextSizeMultiplier)); + } + + [Benchmark] + public void Parse() + { + parser.Parse(fileContent); + } +} \ No newline at end of file diff --git a/cs/BenchmarkMarkdown/MdBenchmark.cs b/cs/BenchmarkMarkdown/MdBenchmark.cs new file mode 100644 index 000000000..2e7af6445 --- /dev/null +++ b/cs/BenchmarkMarkdown/MdBenchmark.cs @@ -0,0 +1,31 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Markdown; + +namespace BenchmarkMarkdown; + +[MemoryDiagnoser] +public class MdBenchmark +{ + private Md md; + private string fileContent; + + [Params(1, 10, 100, 1000)] public int TextSizeMultiplier { get; set; } + + [GlobalSetup] + public void Setup() + { + md = new Md(); + + var baseDir = AppContext.BaseDirectory; + var filePath = Path.Combine(baseDir, "InputData"); + var baseFileContent = File.ReadAllText(filePath, Encoding.UTF8); + fileContent = string.Concat(Enumerable.Repeat(baseFileContent, TextSizeMultiplier)); + } + + [Benchmark] + public string Render() + { + return md.Render(fileContent); + } +} \ No newline at end of file diff --git a/cs/BenchmarkMarkdown/Program.cs b/cs/BenchmarkMarkdown/Program.cs new file mode 100644 index 000000000..ee1bca1dd --- /dev/null +++ b/cs/BenchmarkMarkdown/Program.cs @@ -0,0 +1,5 @@ +using BenchmarkDotNet.Running; +using BenchmarkMarkdown; + +BenchmarkRunner.Run(); +BenchmarkRunner.Run(); \ No newline at end of file diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index 6224fe1bc..8a1c4351a 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -12,6 +12,8 @@ public class MarkdownParser : IParser { TagType.None, new NoneMarkdownTag() } //{ TagType.Link, "" }, }; + + private readonly List possibleTags = []; public IEnumerable Parse(string text) { @@ -291,9 +293,9 @@ private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosi return new Token(TagType.None, text.Substring(startPosition, endPosition - startPosition)); } - private static MarkdownTag GetTag(string text, int position, Stack tokensWithOpenTag) + private MarkdownTag GetTag(string text, int position, Stack tokensWithOpenTag) { - var possibleTags = new List(); + possibleTags.Clear(); foreach (var tag in markdownTagsByType.Values) { diff --git a/cs/Markdown/PlatformType.cs b/cs/Markdown/PlatformType.cs deleted file mode 100644 index df2b59202..000000000 --- a/cs/Markdown/PlatformType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Markdown; - -public enum PlatformType -{ - Unknown, - Windows, - Unix, -} \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs index a16044a5a..3d0af8e1e 100644 --- a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs @@ -4,9 +4,6 @@ public class BoldMarkdownTag() : MarkdownTag(TagType.Bold, "__", true) { public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) { - if (TagType != TagType.Bold && TagType != TagType.Italic) - throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); - var isSatisfiesConditions = false; if (isSameTagAlreadyOpen) isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); diff --git a/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs index daa46d170..0cdb7e54b 100644 --- a/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs @@ -4,9 +4,6 @@ public class ItalicMarkdownTag() : MarkdownTag(TagType.Italic, "_", true) { public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) { - if (TagType != TagType.Bold && TagType != TagType.Italic) - throw new ArgumentException("Tag Italic or Bold was expected, but another tag was received"); - var isSatisfiesConditions = false; if (isSameTagAlreadyOpen) isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); diff --git a/cs/clean-code.sln b/cs/clean-code.sln index 3326411d0..9e2352620 100644 --- a/cs/clean-code.sln +++ b/cs/clean-code.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdown", "Markdown\Markdown.csproj", "{8794FD77-5E36-4990-A3CC-AB363934F2CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkMarkdown", "BenchmarkMarkdown\BenchmarkMarkdown.csproj", "{A85DA5D4-13A8-439F-A81A-CB37962627DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,5 +35,9 @@ Global {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {8794FD77-5E36-4990-A3CC-AB363934F2CC}.Release|Any CPU.Build.0 = Release|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A85DA5D4-13A8-439F-A81A-CB37962627DC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 01114f2704efdfa9d4ea418684cfe4d964c86a05 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sat, 15 Nov 2025 22:11:38 +0500 Subject: [PATCH 15/19] Refactoring --- cs/Markdown/OpenToken.cs | 8 + cs/Markdown/Parser/MarkdownParser.cs | 143 ++++-------------- cs/Markdown/StackExtensions.cs | 9 ++ .../Tag/MarkdownTag/BoldMarkdownTag.cs | 59 +++++++- .../Tag/MarkdownTag/EndOfLineMarkdownTag.cs | 11 +- .../Tag/MarkdownTag/EscapingMarkdownTag.cs | 4 +- .../Tag/MarkdownTag/HeaderMarkdownTag.cs | 15 +- .../Tag/MarkdownTag/ItalicMarkdownTag.cs | 61 +++++++- cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs | 13 +- .../Tag/MarkdownTag/NoneMarkdownTag.cs | 2 +- cs/Markdown/Tests/MarkdownParserTests.cs | 13 +- cs/Markdown/Tests/MdTests.cs | 5 +- 12 files changed, 191 insertions(+), 152 deletions(-) create mode 100644 cs/Markdown/OpenToken.cs create mode 100644 cs/Markdown/StackExtensions.cs diff --git a/cs/Markdown/OpenToken.cs b/cs/Markdown/OpenToken.cs new file mode 100644 index 000000000..ae569c1ea --- /dev/null +++ b/cs/Markdown/OpenToken.cs @@ -0,0 +1,8 @@ +namespace Markdown; + +public class OpenToken(MarkdownTag openTag, int textStartPosition) +{ + public MarkdownTag OpenTag = openTag; + public int TextStartPosition = textStartPosition; + public readonly List NestedTokens = []; +} \ No newline at end of file diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index 8a1c4351a..ae48bf256 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -12,7 +12,7 @@ public class MarkdownParser : IParser { TagType.None, new NoneMarkdownTag() } //{ TagType.Link, "" }, }; - + private readonly List possibleTags = []; public IEnumerable Parse(string text) @@ -42,7 +42,7 @@ public IEnumerable Parse(string text) } if (currentTag.TagType == TagType.EndOfLine && - tokensWithOpenTag.Select(a => a.OpenTag.TagType).Contains(TagType.Header)) + tokensWithOpenTag.Any(a => a.OpenTag.TagType == TagType.Header)) openTokenWithEmptyTag = new OpenToken(markdownTagsByType[TagType.None], i); if (currentTag.IsPairedTag) @@ -57,14 +57,6 @@ public IEnumerable Parse(string text) return result; } - - private static bool ContainsTagType(Stack tokensWithOpenTag, TagType tagType) => - tokensWithOpenTag.Any(t => t.OpenTag.TagType == tagType); - - - private static bool IsTopTagType(Stack tokensWithOpenTag, TagType tagType) => - tokensWithOpenTag.Count > 0 && tokensWithOpenTag.Peek().OpenTag.TagType == tagType; - #region Processing Tags @@ -98,18 +90,19 @@ private static void ProcessUnpairedTag(string text, MarkdownTag currentTag, Stac } } - private static void ProcessTagEndOfLine(string text, Stack stack, int position, List result) + private static void ProcessTagEndOfLine(string text, Stack tokensWithOpenTag, int position, + List result) { - if (!ContainsTagType(stack, TagType.Header)) + if (tokensWithOpenTag.All(t => t.OpenTag.TagType != TagType.Header)) return; - while (stack.Count > 0 && !IsTopTagType(stack, TagType.Header)) - stack.Pop(); + while (!tokensWithOpenTag.IsPeekTagType(TagType.Header)) + tokensWithOpenTag.Pop(); - if (stack.Count > 0) + if (tokensWithOpenTag.Count > 0) { - var headerToken = stack.Pop(); - AddToken(CreateToken(text, position, headerToken), stack, result); + var headerToken = tokensWithOpenTag.Pop(); + AddToken(CreateToken(text, position, headerToken), tokensWithOpenTag, result); } } @@ -119,12 +112,12 @@ private static void ProcessPairedTag(string text, MarkdownTag currentTag, Stack< if (!currentTag.IsPairedTag) throw new ArgumentException("Paired tag was expected, but unpaired tag was received"); - if (IsTopTagType(tokensWithOpenTag, currentTag.TagType) && tokensWithOpenTag.Peek().OpenTag.Equals(currentTag)) + if (tokensWithOpenTag.IsPeekTagType(currentTag.TagType)) { var openToken = tokensWithOpenTag.Pop(); AddToken(CreateToken(text, position, openToken), tokensWithOpenTag, result); } - else if (!ContainsTagType(tokensWithOpenTag, currentTag.TagType)) + else if (tokensWithOpenTag.All(t => t.OpenTag.TagType != currentTag.TagType)) { tokensWithOpenTag.Push(new OpenToken(currentTag, position + currentTag.TagText.Length)); } @@ -134,87 +127,23 @@ private static void ProcessPairedTag(string text, MarkdownTag currentTag, Stack< #region BorderlineCases - /// - /// true - if the Bold or Italic tags are located inside words; - /// - private static bool CheckOpenTokenTagBoldOrItalicLocatedInsideWords(string text, int endPosition, - OpenToken openToken) - { - if (openToken.OpenTag.TagType != TagType.Bold && openToken.OpenTag.TagType != TagType.Italic) return false; - - var tagLength = openToken.OpenTag.TagText.Length; - var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; - - var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && - !char.IsWhiteSpace(text[openToken.TextStartPosition]) && - !char.IsWhiteSpace(text[symbolBeforeStartTag]); - var isEndTagInMiddleWord = endPosition + tagLength < text.Length && - !char.IsWhiteSpace(text[endPosition + tagLength]) && - !char.IsWhiteSpace(text[endPosition - 1]); - var textContainsSeveralWords = false; - - var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); - - foreach (var symbol in part) - if (char.IsWhiteSpace(symbol)) - { - textContainsSeveralWords = true; - break; - } - - return (isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords; - } - - private static bool IsTokenHighlightsPartOfWordWithDigits(string text, int endPosition, OpenToken openToken) - { - var tagLength = openToken.OpenTag.TagText.Length; - var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; - var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && - !char.IsWhiteSpace(text[openToken.TextStartPosition]) && - !char.IsWhiteSpace(text[symbolBeforeStartTag]); - var isEndTagInMiddleWord = endPosition + tagLength < text.Length && - !char.IsWhiteSpace(text[endPosition + tagLength]) && - !char.IsWhiteSpace(text[endPosition - 1]); - - var isOnlyPartOfWordHighlighted = isStartTagInMiddleWord || isEndTagInMiddleWord; - var partText = text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); - - return isOnlyPartOfWordHighlighted && partText.All(char.IsLetterOrDigit) && partText.Any(char.IsDigit); - } - - private static Token ProcessEmptyUnderscores(Token token) - { - if (token is { TagType: TagType.Bold, Content.Length: 0, Children: null }) - token = new Token(TagType.None, - string.Concat(Enumerable.Repeat(markdownTagsByType[TagType.Bold].TagText, 2))); - - return token; - } - - private static Token ConvertTagBoldInsideTagItalic(Token token) + private static OpenToken ConvertTagBoldInsideTagItalic(OpenToken openToken) { - if (token is { TagType: TagType.Italic, Children: not null }) - { - var result = new Token(TagType.Italic, token.Content, []); - foreach (var child in token.Children) result.Children!.Add(ConvertTagBoldToTagNone(child)); - return result; - } - - if (token is { Children: not null }) - for (var i = 0; i < token.Children.Count; i++) - token.Children[i] = ConvertTagBoldInsideTagItalic(token.Children[i]); + if (openToken.OpenTag.TagType == TagType.Italic && openToken.NestedTokens.Count > 0) + for (var i = 0; i < openToken.NestedTokens.Count; i++) + openToken.NestedTokens[i] = ConvertTagBoldToTagNone(openToken.NestedTokens[i]); - return token; + return openToken; } private static Token ConvertTagBoldToTagNone(Token token) { if (token.TagType == TagType.Bold) { - var tagContent = markdownTagsByType[TagType.Bold].TagText; - var newContent = $"{tagContent}{token.Content}{tagContent}"; + var tagText = markdownTagsByType[TagType.Bold].TagText; + var newText = $"{tagText}{token.Content}{tagText}"; var children = token.Children?.Select(ConvertTagBoldToTagNone).ToList(); - return new Token(TagType.None, newContent, children); + return new Token(TagType.None, newText, children); } if (token.Children != null) @@ -267,24 +196,20 @@ private static Token CreateToken(string text, int endPosition, OpenToken openTok { var length = endPosition - openToken.TextStartPosition; - var result = new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length), - openToken.NestedTokens); + if (openToken.OpenTag.TagType == TagType.Italic) openToken = ConvertTagBoldInsideTagItalic(openToken); - if (CheckOpenTokenTagBoldOrItalicLocatedInsideWords(text, endPosition, openToken) || - (openToken.OpenTag.TagType is TagType.Bold or TagType.Italic && - IsTokenHighlightsPartOfWordWithDigits(text, endPosition, openToken))) + if (!markdownTagsByType[openToken.OpenTag.TagType].IsTagCorrect(text, endPosition, openToken)) { - var tagContent = openToken.OpenTag.TagText; - var content = $"{tagContent}{text.Substring(openToken.TextStartPosition, length)}{tagContent}"; + var tagText = openToken.OpenTag.TagText; + var content = $"{tagText}{text.Substring(openToken.TextStartPosition, length)}{tagText}"; return new Token(TagType.None, content); } if (openToken.NestedTokens is [{ TagType: TagType.None }] || openToken.NestedTokens.Count == 0) - result = new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length)); + return new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length)); - result = ProcessEmptyUnderscores(result); - result = ConvertTagBoldInsideTagItalic(result); - return result; + return new Token(openToken.OpenTag.TagType, text.Substring(openToken.TextStartPosition, length), + openToken.NestedTokens); } private static Token CreateTokenForPairedTagWithoutPair(string text, int endPosition, OpenToken openToken) @@ -298,19 +223,9 @@ private MarkdownTag GetTag(string text, int position, Stack tokensWit possibleTags.Clear(); foreach (var tag in markdownTagsByType.Values) - { - if (tag.TagText.Length + position > text.Length) continue; - - if (tag.IsTag(text, position, ContainsTagType(tokensWithOpenTag, tag.TagType))) possibleTags.Add(tag); - } + if (tag.IsStartOfTag(text, position, tokensWithOpenTag.Any(t => t.OpenTag.TagType == tag.TagType))) + possibleTags.Add(tag); return possibleTags.OrderByDescending(x => x.TagText.Length).First(); } - - private class OpenToken(MarkdownTag openTag, int textStartPosition) - { - public readonly MarkdownTag OpenTag = openTag; - public readonly int TextStartPosition = textStartPosition; - public readonly List NestedTokens = []; - } } \ No newline at end of file diff --git a/cs/Markdown/StackExtensions.cs b/cs/Markdown/StackExtensions.cs new file mode 100644 index 000000000..e6fd10d0c --- /dev/null +++ b/cs/Markdown/StackExtensions.cs @@ -0,0 +1,9 @@ +namespace Markdown; + +public static class StackExtensions +{ + public static bool IsPeekTagType(this Stack tokensWithOpenTag, TagType tagType) + { + return tokensWithOpenTag.Count > 0 && tokensWithOpenTag.Peek().OpenTag.TagType == tagType; + } +} diff --git a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs index 3d0af8e1e..cf8777cc9 100644 --- a/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/BoldMarkdownTag.cs @@ -2,8 +2,12 @@ public class BoldMarkdownTag() : MarkdownTag(TagType.Bold, "__", true) { - public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + public new static readonly string TagText = "__"; + + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { + if (TagText.Length + position > text.Length) return false; + var isSatisfiesConditions = false; if (isSameTagAlreadyOpen) isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); @@ -14,4 +18,57 @@ public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isSatisfiesConditions; } + + public override bool IsTagCorrect(string text, int endPosition, OpenToken openToken) + { + if (openToken.OpenTag.TagType != TagType.Bold) + throw new ArgumentException($"{openToken.OpenTag.TagType.ToString()} should be {nameof(TagType.Bold)}"); + + var isTokenEmpty = openToken.TextStartPosition == endPosition && openToken.NestedTokens.Count == 0; + + return !IsTokenLocatedInsideWords(text, endPosition, openToken) && + !IsTokenHighlightsPartOfWordWithDigits(text, endPosition, openToken) && !isTokenEmpty; + } + + private bool IsTokenLocatedInsideWords(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + var textContainsSeveralWords = false; + + var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + foreach (var symbol in part) + if (char.IsWhiteSpace(symbol)) + { + textContainsSeveralWords = true; + break; + } + + return (isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords; + } + + private bool IsTokenHighlightsPartOfWordWithDigits(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + + var isOnlyPartOfWordHighlighted = isStartTagInMiddleWord || isEndTagInMiddleWord; + var partText = text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + return isOnlyPartOfWordHighlighted && partText.All(char.IsLetterOrDigit) && partText.Any(char.IsDigit); + } } \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs index 6e264b971..141b209d9 100644 --- a/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/EndOfLineMarkdownTag.cs @@ -1,13 +1,12 @@ namespace Markdown; -public class EndOfLineMarkdownTag() : MarkdownTag(TagType.EndOfLine, "\n", false) +public class EndOfLineMarkdownTag() : MarkdownTag(TagType.EndOfLine, Environment.NewLine, false) { - public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { - if (text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal)) - { - return true; - } + if (TagText.Length + position > text.Length) return false; + + if (text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal)) return true; return false; } } \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs index c3a7e0e02..5f4f1cddb 100644 --- a/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/EscapingMarkdownTag.cs @@ -4,9 +4,9 @@ public class EscapingMarkdownTag() : MarkdownTag(TagType.Escaping, "\\", false, { private static readonly List escapeSymbols = ['\\', '#', '_']; - public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { - if (text.Length <= position + 1) return false; + if (TotalTagLength + position > text.Length) return false; var isCanEscaping = escapeSymbols.Contains(text[position + 1]); diff --git a/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs index dbd51e0d3..c2802f200 100644 --- a/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs @@ -2,18 +2,15 @@ public class HeaderMarkdownTag() : MarkdownTag(TagType.Header, "# ", false) { - public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { - var doubleNewLineUnix = "\n\n"; - var doubleNewLineWindows = "\r\n\r\n"; + if (TagText.Length + position > text.Length) return false; - var isNewParagraphWindows = position == 0 || (position >= 4 && text.AsSpan(position - 4, 4) - .Equals(doubleNewLineWindows, StringComparison.Ordinal)); + var doubleNewLine = string.Concat(Enumerable.Repeat(Environment.NewLine, 2)); + var length = doubleNewLine.Length; - var isNewParagraphUnix = position == 0 || (position >= 2 && text.AsSpan(position - 2, 2) - .Equals(doubleNewLineUnix, StringComparison.Ordinal)); - - var isNewParagraph = isNewParagraphWindows || isNewParagraphUnix; + var isNewParagraph = position == 0 || (position >= length && text.AsSpan(position - length, length) + .Equals(doubleNewLine, StringComparison.Ordinal)); return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isNewParagraph && !isSameTagAlreadyOpen; diff --git a/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs index 0cdb7e54b..debc70902 100644 --- a/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/ItalicMarkdownTag.cs @@ -2,8 +2,10 @@ public class ItalicMarkdownTag() : MarkdownTag(TagType.Italic, "_", true) { - public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { + if (TagText.Length + position > text.Length) return false; + var isSatisfiesConditions = false; if (isSameTagAlreadyOpen) isSatisfiesConditions = !char.IsWhiteSpace(text[position - 1]); @@ -13,10 +15,63 @@ public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) isSatisfiesConditions = true; var isContainsTagItalic = text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal); - var tagBoldLength = new BoldMarkdownTag().TagText.Length; - var tagBoldText = new BoldMarkdownTag().TagText; + var tagBoldLength = BoldMarkdownTag.TagText.Length; + var tagBoldText = BoldMarkdownTag.TagText; var isContainsTagBold = tagBoldLength + position <= text.Length && text.AsSpan(position, tagBoldLength) .Equals(tagBoldText, StringComparison.Ordinal); return isContainsTagItalic && !isContainsTagBold && isSatisfiesConditions; } + + public override bool IsTagCorrect(string text, int endPosition, OpenToken openToken) + { + if (openToken.OpenTag.TagType != TagType.Italic) + throw new ArgumentException($"{openToken.OpenTag.TagType.ToString()} should be {nameof(TagType.Italic)}"); + + var isTokenEmpty = openToken.TextStartPosition == endPosition && openToken.NestedTokens.Count == 0; + + return !IsTokenLocatedInsideWords(text, endPosition, openToken) && + !IsTokenHighlightsPartOfWordWithDigits(text, endPosition, openToken) && !isTokenEmpty; + } + + private bool IsTokenLocatedInsideWords(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + var textContainsSeveralWords = false; + + var part = text.AsSpan(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + foreach (var symbol in part) + if (char.IsWhiteSpace(symbol)) + { + textContainsSeveralWords = true; + break; + } + + return (isStartTagInMiddleWord || isEndTagInMiddleWord) && textContainsSeveralWords; + } + + private bool IsTokenHighlightsPartOfWordWithDigits(string text, int endPosition, OpenToken openToken) + { + var tagLength = TagText.Length; + var symbolBeforeStartTag = openToken.TextStartPosition - tagLength - 1; + var isStartTagInMiddleWord = symbolBeforeStartTag >= 0 && + !char.IsWhiteSpace(text[openToken.TextStartPosition]) && + !char.IsWhiteSpace(text[symbolBeforeStartTag]); + var isEndTagInMiddleWord = endPosition + tagLength < text.Length && + !char.IsWhiteSpace(text[endPosition + tagLength]) && + !char.IsWhiteSpace(text[endPosition - 1]); + + var isOnlyPartOfWordHighlighted = isStartTagInMiddleWord || isEndTagInMiddleWord; + var partText = text.Substring(openToken.TextStartPosition, endPosition - openToken.TextStartPosition); + + return isOnlyPartOfWordHighlighted && partText.All(char.IsLetterOrDigit) && partText.Any(char.IsDigit); + } } \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs index 9f514c62b..261d82f48 100644 --- a/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/MarkdownTag.cs @@ -3,9 +3,14 @@ public abstract class MarkdownTag(TagType tagType, string tagText, bool isPairedTag, int? tagLength = null) { public TagType TagType { get; } = tagType; - public virtual string TagText { get; } = tagText; - public virtual bool IsPairedTag { get; } = isPairedTag; - public virtual int TotalTagLength { get; init; } = tagLength ?? tagText.Length; + public string TagText { get; } = tagText; + public bool IsPairedTag { get; } = isPairedTag; + public int TotalTagLength { get; } = tagLength ?? tagText.Length; - public abstract bool IsTag(string text, int position, bool isSameTagAlreadyOpen); + public abstract bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen); + + public virtual bool IsTagCorrect(string text, int endPosition, OpenToken openToken) + { + return true; + } } \ No newline at end of file diff --git a/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs index 3ffa86a9b..65bbbe930 100644 --- a/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/NoneMarkdownTag.cs @@ -2,7 +2,7 @@ public class NoneMarkdownTag() : MarkdownTag(TagType.None, "", false) { - public override bool IsTag(string text, int position, bool isSameTagAlreadyOpen) + public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { return true; } diff --git a/cs/Markdown/Tests/MarkdownParserTests.cs b/cs/Markdown/Tests/MarkdownParserTests.cs index 011739965..85d3bfd60 100644 --- a/cs/Markdown/Tests/MarkdownParserTests.cs +++ b/cs/Markdown/Tests/MarkdownParserTests.cs @@ -202,22 +202,17 @@ public static IEnumerable CasesTextContainsHeaderTag() yield return new TestCaseData("# wordA # ", new[] { new Token(TagType.Header, "wordA # ") }); yield return new TestCaseData(" # wordA", new[] { new Token(TagType.None, " # wordA") }); - yield return new TestCaseData("# wordA\n # ", + yield return new TestCaseData($"# wordA{Environment.NewLine} # ", new[] { new Token(TagType.Header, "wordA"), - new Token(TagType.None, "\n # ") + new Token(TagType.None, $"{Environment.NewLine} # ") }); - yield return new TestCaseData(" wordA\n\n# wordB", + yield return new TestCaseData($" wordA{Environment.NewLine}{Environment.NewLine}# wordB", new[] { - new Token(TagType.None, " wordA\n\n"), new Token(TagType.Header, "wordB") - }); - yield return new TestCaseData(" wordA\r\n\r\n# wordB", - new[] - { - new Token(TagType.None, " wordA\r\n\r\n"), new Token(TagType.Header, "wordB") + new Token(TagType.None, $" wordA{Environment.NewLine}{Environment.NewLine}"), new Token(TagType.Header, "wordB") }); } diff --git a/cs/Markdown/Tests/MdTests.cs b/cs/Markdown/Tests/MdTests.cs index a712f97fb..bd7c67577 100644 --- a/cs/Markdown/Tests/MdTests.cs +++ b/cs/Markdown/Tests/MdTests.cs @@ -163,9 +163,8 @@ public static IEnumerable CasesTextContainsHeaderTag() yield return new TestCaseData("# wordA", "

wordA

"); yield return new TestCaseData("# wordA # ", "

wordA #

"); yield return new TestCaseData(" # wordA", " # wordA"); - yield return new TestCaseData("# wordA\n # ", "

wordA

\n # "); - yield return new TestCaseData(" wordA\n\n# wordB", " wordA\n\n

wordB

"); - yield return new TestCaseData(" wordA\r\n\r\n# wordB", " wordA\r\n\r\n

wordB

"); + yield return new TestCaseData($"# wordA{Environment.NewLine} # ", $"

wordA

{Environment.NewLine} # "); + yield return new TestCaseData($" wordA{Environment.NewLine}{Environment.NewLine}# wordB", $" wordA{Environment.NewLine}{Environment.NewLine}

wordB

"); } public static IEnumerable CasesWhenTextContainsEscapingTag() From 78bcec4bba7b0ec064478d8c7be4a37ae2bda6d7 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sun, 16 Nov 2025 12:15:18 +0500 Subject: [PATCH 16/19] Refactoring --- cs/Markdown/Parser/MarkdownParser.cs | 24 ++++--------------- cs/Markdown/StackExtensions.cs | 2 +- .../Tag/MarkdownTag/HeaderMarkdownTag.cs | 20 ++++++++++++++-- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cs/Markdown/Parser/MarkdownParser.cs b/cs/Markdown/Parser/MarkdownParser.cs index ae48bf256..587316ac3 100644 --- a/cs/Markdown/Parser/MarkdownParser.cs +++ b/cs/Markdown/Parser/MarkdownParser.cs @@ -77,7 +77,7 @@ private static void ProcessUnpairedTag(string text, MarkdownTag currentTag, Stac break; } case TagType.EndOfLine: - ProcessTagEndOfLine(text, tokensWithOpenTag, position, result); + HeaderMarkdownTag.ProcessEndTag(text, tokensWithOpenTag, position, result); break; case TagType.Escaping: { @@ -90,29 +90,13 @@ private static void ProcessUnpairedTag(string text, MarkdownTag currentTag, Stac } } - private static void ProcessTagEndOfLine(string text, Stack tokensWithOpenTag, int position, - List result) - { - if (tokensWithOpenTag.All(t => t.OpenTag.TagType != TagType.Header)) - return; - - while (!tokensWithOpenTag.IsPeekTagType(TagType.Header)) - tokensWithOpenTag.Pop(); - - if (tokensWithOpenTag.Count > 0) - { - var headerToken = tokensWithOpenTag.Pop(); - AddToken(CreateToken(text, position, headerToken), tokensWithOpenTag, result); - } - } - private static void ProcessPairedTag(string text, MarkdownTag currentTag, Stack tokensWithOpenTag, int position, List result) { if (!currentTag.IsPairedTag) throw new ArgumentException("Paired tag was expected, but unpaired tag was received"); - if (tokensWithOpenTag.IsPeekTagType(currentTag.TagType)) + if (tokensWithOpenTag.IsPeekEqual(currentTag.TagType)) { var openToken = tokensWithOpenTag.Pop(); AddToken(CreateToken(text, position, openToken), tokensWithOpenTag, result); @@ -184,7 +168,7 @@ private static List AddUnfinishedTags(string text, OpenToken? openTokenWi return listTokens; } - private static void AddToken(Token token, Stack tokensWithOpenTag, List result) + public static void AddToken(Token token, Stack tokensWithOpenTag, List result) { if (tokensWithOpenTag.Count == 0) result.Add(token); @@ -192,7 +176,7 @@ private static void AddToken(Token token, Stack tokensWithOpenTag, Li tokensWithOpenTag.Peek().NestedTokens.Add(token); } - private static Token CreateToken(string text, int endPosition, OpenToken openToken) + public static Token CreateToken(string text, int endPosition, OpenToken openToken) { var length = endPosition - openToken.TextStartPosition; diff --git a/cs/Markdown/StackExtensions.cs b/cs/Markdown/StackExtensions.cs index e6fd10d0c..d11e0e679 100644 --- a/cs/Markdown/StackExtensions.cs +++ b/cs/Markdown/StackExtensions.cs @@ -2,7 +2,7 @@ public static class StackExtensions { - public static bool IsPeekTagType(this Stack tokensWithOpenTag, TagType tagType) + public static bool IsPeekEqual(this Stack tokensWithOpenTag, TagType tagType) { return tokensWithOpenTag.Count > 0 && tokensWithOpenTag.Peek().OpenTag.TagType == tagType; } diff --git a/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs index c2802f200..928f11fc7 100644 --- a/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs +++ b/cs/Markdown/Tag/MarkdownTag/HeaderMarkdownTag.cs @@ -5,14 +5,30 @@ public class HeaderMarkdownTag() : MarkdownTag(TagType.Header, "# ", false) public override bool IsStartOfTag(string text, int position, bool isSameTagAlreadyOpen) { if (TagText.Length + position > text.Length) return false; - + var doubleNewLine = string.Concat(Enumerable.Repeat(Environment.NewLine, 2)); var length = doubleNewLine.Length; - + var isNewParagraph = position == 0 || (position >= length && text.AsSpan(position - length, length) .Equals(doubleNewLine, StringComparison.Ordinal)); return text.AsSpan(position, TagText.Length).Equals(TagText, StringComparison.Ordinal) && isNewParagraph && !isSameTagAlreadyOpen; } + + public static void ProcessEndTag(string text, Stack tokensWithOpenTag, int position, + List result) + { + if (tokensWithOpenTag.All(t => t.OpenTag.TagType != TagType.Header)) + return; + + while (!tokensWithOpenTag.IsPeekEqual(TagType.Header)) + tokensWithOpenTag.Pop(); + + if (tokensWithOpenTag.Count > 0) + { + var headerToken = tokensWithOpenTag.Pop(); + MarkdownParser.AddToken(MarkdownParser.CreateToken(text, position, headerToken), tokensWithOpenTag, result); + } + } } \ No newline at end of file From 52ecd3c2f435dfb2d74c57ba4f11d9a9984b376f Mon Sep 17 00:00:00 2001 From: Ruslan Date: Sun, 30 Nov 2025 18:40:03 +0500 Subject: [PATCH 17/19] Added Link --- ...hmarkMarkdown.MdBenchmark-report-github.md | 12 +-- .../BenchmarkMarkdown.MdBenchmark-report.csv | 8 +- .../BenchmarkMarkdown.MdBenchmark-report.html | 12 +-- cs/BenchmarkMarkdown/InputData | 2 + cs/Markdown/Parser/MarkdownParser.cs | 26 +++++- cs/Markdown/Renderer/HtmlRenderer.cs | 16 +++- cs/Markdown/Tag/{ => HtmlTag}/HtmlTag.cs | 0 cs/Markdown/Tag/HtmlTag/LinkHtmlTag.cs | 11 +++ .../Tag/MarkdownTag/LinkMarkdownTag.cs | 88 +++++++++++++++++++ cs/Markdown/Tests/MarkdownParserTests.cs | 25 ++++++ cs/Markdown/Tests/MdTests.cs | 24 +++++ cs/Markdown/{ => Token}/Token.cs | 2 +- cs/Markdown/Token/TokenTagLink.cs | 7 ++ 13 files changed, 211 insertions(+), 22 deletions(-) rename cs/Markdown/Tag/{ => HtmlTag}/HtmlTag.cs (100%) create mode 100644 cs/Markdown/Tag/HtmlTag/LinkHtmlTag.cs create mode 100644 cs/Markdown/Tag/MarkdownTag/LinkMarkdownTag.cs rename cs/Markdown/{ => Token}/Token.cs (77%) create mode 100644 cs/Markdown/Token/TokenTagLink.cs diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md index 8cb740937..f186405f0 100644 --- a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report-github.md @@ -8,9 +8,9 @@ AMD Ryzen 9 5900HX with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physica ``` -| Method | TextSizeMultiplier | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------- |------------------- |-------------:|------------:|-------------:|------------:|-----------:|----------:|-----------:| -| **Render** | **1** | **308.1 μs** | **4.70 μs** | **3.92 μs** | **125.4883** | **2.9297** | **-** | **1 MB** | -| **Render** | **10** | **2,951.1 μs** | **25.49 μs** | **22.59 μs** | **1246.0938** | **191.4063** | **3.9063** | **9.96 MB** | -| **Render** | **100** | **33,804.5 μs** | **547.76 μs** | **512.37 μs** | **13666.6667** | **400.0000** | **200.0000** | **109.4 MB** | -| **Render** | **1000** | **351,464.0 μs** | **6,917.49 μs** | **11,557.58 μs** | **140000.0000** | **36000.0000** | **4000.0000** | **1093.79 MB** | +| Method | TextSizeMultiplier | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------- |------------------- |-------------:|-------------:|-------------:|------------:|-----------:|----------:|-----------:| +| **Render** | **1** | **500.1 μs** | **9.93 μs** | **17.13 μs** | **150.3906** | **2.9297** | **-** | **1.2 MB** | +| **Render** | **10** | **5,322.4 μs** | **105.85 μs** | **273.24 μs** | **1492.1875** | **195.3125** | **-** | **11.95 MB** | +| **Render** | **100** | **56,038.2 μs** | **1,117.32 μs** | **1,566.33 μs** | **14900.0000** | **500.0000** | **200.0000** | **119.28 MB** | +| **Render** | **1000** | **576,250.8 μs** | **11,258.63 μs** | **11,561.78 μs** | **149000.0000** | **31000.0000** | **4000.0000** | **1192.54 MB** | diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv index 7973c1d5f..956d9f60f 100644 --- a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.csv @@ -1,5 +1,5 @@ Method;Job;AnalyzeLaunchVariance;EvaluateOverhead;MaxAbsoluteError;MaxRelativeError;MinInvokeCount;MinIterationTime;OutlierMode;Affinity;EnvironmentVariables;Jit;LargeAddressAware;Platform;PowerPlanMode;Runtime;AllowVeryLargeObjects;Concurrent;CpuGroups;Force;HeapAffinitizeMask;HeapCount;NoAffinitize;RetainVm;Server;Arguments;BuildConfiguration;Clock;EngineFactory;NuGetReferences;Toolchain;IsMutator;InvocationCount;IterationCount;IterationTime;LaunchCount;MaxIterationCount;MaxWarmupIterationCount;MemoryRandomization;MinIterationCount;MinWarmupIterationCount;RunStrategy;UnrollFactor;WarmupCount;TextSizeMultiplier;Mean;Error;StdDev;Gen0;Gen1;Gen2;Allocated -Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1;308.1 μs;4.70 μs;3.92 μs;125.4883;2.9297;0.0000;1 MB -Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;10;"2,951.1 μs";25.49 μs;22.59 μs;1246.0938;191.4063;3.9063;9.96 MB -Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;100;"33,804.5 μs";547.76 μs;512.37 μs;13666.6667;400.0000;200.0000;109.4 MB -Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1000;"351,464.0 μs";"6,917.49 μs";"11,557.58 μs";140000.0000;36000.0000;4000.0000;1093.79 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1;500.1 μs;9.93 μs;17.13 μs;150.3906;2.9297;0.0000;1.2 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;10;"5,322.4 μs";105.85 μs;273.24 μs;1492.1875;195.3125;0.0000;11.95 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;100;"56,038.2 μs";"1,117.32 μs";"1,566.33 μs";14900.0000;500.0000;200.0000;119.28 MB +Render;DefaultJob;False;Default;Default;Default;Default;Default;Default;1111111111111111;Empty;RyuJit;Default;X64;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c;.NET 9.0;False;True;False;True;Default;Default;False;False;False;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;Default;16;Default;1000;"576,250.8 μs";"11,258.63 μs";"11,561.78 μs";149000.0000;31000.0000;4000.0000;1192.54 MB diff --git a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html index 22df66e7a..17cd9cb08 100644 --- a/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html +++ b/cs/BenchmarkMarkdown/BenchmarkDotNet.Artifacts/results/BenchmarkMarkdown.MdBenchmark-report.html @@ -2,7 +2,7 @@ -BenchmarkMarkdown.MdBenchmark-20251115-183217 +BenchmarkMarkdown.MdBenchmark-20251130-183500