diff --git a/src/MarkdownParser/IViewSupplier.cs b/src/MarkdownParser/IViewSupplier.cs index 2c7e231..522bb28 100644 --- a/src/MarkdownParser/IViewSupplier.cs +++ b/src/MarkdownParser/IViewSupplier.cs @@ -4,6 +4,12 @@ namespace MarkdownParser { public interface IViewSupplier { + /// + /// get a textual line break + /// + /// + string GetTextualLineBreak(); + /// /// a default text view /// @@ -72,5 +78,30 @@ public interface IViewSupplier /// placeholder string /// T GetPlaceholder(string placeholderName); + + /// + /// a view that shows fenced code (found in MD blocks starting with ```cs ) + /// + /// + T GetFencedCodeBlock(string content, string codeInfo); + + /// + /// a view that shows indented code (found in MD lines starting with at least 4 spaces) + /// + /// + T GetIndentedCodeBlock(string content); + + /// + /// a view that shows html content + /// + /// + T GetHtmlBlock(string content); + + /// + /// a view that shows reference definitions ([link]s usually at the end of the document) + /// + /// collection of Reference Definitions + /// + T GetReferenceDefinitions(IEnumerable markdownReferenceDefinitions); } } diff --git a/src/MarkdownParser/ListBulletFormatter.cs b/src/MarkdownParser/ListBulletFormatter.cs deleted file mode 100644 index 6a96d6a..0000000 --- a/src/MarkdownParser/ListBulletFormatter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Globalization; - -namespace MarkdownParser -{ - public static class ListBulletFormatter - { - public static string GetListItemBullet(bool isOrderedList, int sequenceNumber, int listLevel, string listItemBulletOddCharacter, string listItemBulletEvenCharacter) - { - // 'listlevel' even or odd (list start at level 1 == odd) - var isListOddLeveled = (listLevel % 2) != 0; - - if (!isOrderedList) - { - return (isListOddLeveled) - ? listItemBulletOddCharacter - : listItemBulletEvenCharacter; - } - - const int aCharacterPosition = 97; // character 'a' position in ASCI table - var sequenceNumberCharacterPosition = -1 + aCharacterPosition + sequenceNumber; - - var bullet = (isListOddLeveled) - ? sequenceNumber.ToString() - : Convert.ToChar(sequenceNumberCharacterPosition, CultureInfo.InvariantCulture).ToString(); - - return $"{bullet}."; - } - } -} diff --git a/src/MarkdownParser/MarkdownParser.csproj b/src/MarkdownParser/MarkdownParser.csproj index dc60480..5e485e8 100644 --- a/src/MarkdownParser/MarkdownParser.csproj +++ b/src/MarkdownParser/MarkdownParser.csproj @@ -49,4 +49,10 @@ + + + <_Parameter1>MarkdownParser.Test + + + diff --git a/src/MarkdownParser/MarkdownReferenceDefinition.cs b/src/MarkdownParser/MarkdownReferenceDefinition.cs new file mode 100644 index 0000000..5ab3892 --- /dev/null +++ b/src/MarkdownParser/MarkdownReferenceDefinition.cs @@ -0,0 +1,10 @@ +namespace MarkdownParser +{ + public class MarkdownReferenceDefinition + { + public string Label { get; set; } + public string Url { get; set; } + public string Title { get; set; } + public bool IsPlaceholder { get; set; } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/ViewFormatter.cs b/src/MarkdownParser/ViewFormatter.cs index af12363..851e480 100644 --- a/src/MarkdownParser/ViewFormatter.cs +++ b/src/MarkdownParser/ViewFormatter.cs @@ -17,12 +17,15 @@ public ViewFormatter(IViewSupplier viewSupplier) public List Format(Block markdownBlock) { WriteBlockToView(markdownBlock, _writer); + _writer.StartAndFinalizeReferenceDefinitions(); + return _writer.Flush(); } public List FormatSingleBlock(Block markdownBlock) { WriteBlockToView(markdownBlock, _writer, false); + return _writer.Flush(); } @@ -36,6 +39,7 @@ private void WriteBlockToView(Block block, ViewWriter writer, bool continueWi switch (block.Tag) { case BlockTag.Document: + _writer.RegisterReferenceDefinitions(block.Document.ReferenceMap); WriteBlockToView(block.FirstChild, writer); break; case BlockTag.Paragraph: @@ -69,17 +73,16 @@ private void WriteBlockToView(Block block, ViewWriter writer, bool continueWi writer.StartAndFinalizeThematicBreak(); break; case BlockTag.FencedCode: + writer.StartAndFinalizeFencedCodeBlock(block.StringContent, block.FencedCodeData.Info); + break; case BlockTag.IndentedCode: - // not supported + writer.StartAndFinalizeIndentedCodeBlock(block.StringContent); break; case BlockTag.HtmlBlock: - // TODO.....if needed - //writer.StartBlock(BlockTag.Paragraph, block.StringContent.ToString()); - //WriteBlockToView(block.FirstChild, writer); - //writer.FinalizeParagraphBlock(); + writer.StartAndFinalizeHtmlBlock(block.StringContent); break; case BlockTag.ReferenceDefinition: - // not supported + // ignore, handled at the end of document by _writer.StartAndFinalizeReferenceDefinitions() break; default: throw new CommonMarkException("Block type " + block.Tag + " is not supported.", block); @@ -114,7 +117,7 @@ private void WriteInlineToView(Inline inline, ViewWriter writer) break; case InlineTag.SoftBreak: case InlineTag.LineBreak: - writer.AddText(Environment.NewLine); + writer.AddText(writer.GetTextualLineBreak()); break; case InlineTag.Placeholder: writer.StartAndFinalizePlaceholderBlock(inline.TargetUrl); diff --git a/src/MarkdownParser/ViewWriter.cs b/src/MarkdownParser/ViewWriter.cs index 6eb147d..2578803 100644 --- a/src/MarkdownParser/ViewWriter.cs +++ b/src/MarkdownParser/ViewWriter.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using CommonMark.Syntax; @@ -10,8 +10,14 @@ public class ViewWriter { private IViewSupplier ViewSupplier { get; } private List WrittenViews { get; set; } = new List(); - private Stack> Workbench { get; } = new Stack>(); + private Dictionary _referenceDefinitions; + + public ViewWriter(IViewSupplier viewSupplier) + { + ViewSupplier = viewSupplier; + } + private ViewWriterCache GetWorkbenchItem() { if (Workbench.Count == 0) @@ -22,11 +28,6 @@ private ViewWriterCache GetWorkbenchItem() return Workbench.Peek(); } - public ViewWriter(IViewSupplier viewSupplier) - { - ViewSupplier = viewSupplier; - } - public List Flush() { var collectedViews = WrittenViews; @@ -35,6 +36,11 @@ public List Flush() return collectedViews; } + public void RegisterReferenceDefinitions(Dictionary referenceDefinitions) + { + _referenceDefinitions = referenceDefinitions; + } + public void StartBlock(BlockTag blockType, string content = "") { Workbench.Push(new ViewWriterCache { ComponentType = blockType }); @@ -57,8 +63,9 @@ public void FinalizeParagraphBlock() foreach (var itemsCacheTuple in itemsCache) { - var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) ? - ViewSupplier.GetTextView(itemsCacheTuple.Item1) : itemsCacheTuple.Item2; + var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) + ? ViewSupplier.GetTextView(itemsCacheTuple.Item1) + : itemsCacheTuple.Item2; if (view != null) { @@ -94,7 +101,6 @@ public void FinalizeHeaderBlock(int headerLevel) var wbi = GetWorkbenchItem(); if (wbi.ComponentType != BlockTag.AtxHeading && wbi.ComponentType != BlockTag.SetextHeading) - { Debug.WriteLine($"Finalizing Header can not finalize {wbi.ComponentType}"); return; @@ -107,8 +113,9 @@ public void FinalizeHeaderBlock(int headerLevel) foreach (var itemsCacheTuple in itemsCache) { - var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) ? - ViewSupplier.GetHeaderView(itemsCacheTuple.Item1, headerLevel) : itemsCacheTuple.Item2; + var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) + ? ViewSupplier.GetHeaderView(itemsCacheTuple.Item1, headerLevel) + : itemsCacheTuple.Item2; views.Add(view); } @@ -155,8 +162,9 @@ public void FinalizeListItemBlock(ListData listData) foreach (var itemsCacheTuple in itemsCache) { - var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) ? - ViewSupplier.GetTextView(itemsCacheTuple.Item1) : itemsCacheTuple.Item2; + var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) + ? ViewSupplier.GetTextView(itemsCacheTuple.Item1) + : itemsCacheTuple.Item2; if (view != null) { @@ -164,9 +172,9 @@ public void FinalizeListItemBlock(ListData listData) } } - var flattendView = StackViews(views); + var flattenedView = StackViews(views); - var listItemView = ViewSupplier.GetListItemView(flattendView, isOrderedList, sequenceNumber, depthLevel); + var listItemView = ViewSupplier.GetListItemView(flattenedView, isOrderedList, sequenceNumber, depthLevel); StoreView(listItemView); } @@ -182,10 +190,62 @@ public void StartAndFinalizeImageBlock(string targetUrl, string subscription, st StoreView(imageView); } + public void StartAndFinalizeFencedCodeBlock(StringContent content, string blockInfo) + { + var parsedContent = StringContentToStringWithLineBreaks(content); + + var blockView = ViewSupplier.GetFencedCodeBlock(parsedContent, blockInfo); + StoreView(blockView); + } + + public void StartAndFinalizeIndentedCodeBlock(StringContent content) + { + var parsedContent = StringContentToStringWithLineBreaks(content); + + var blockView = ViewSupplier.GetIndentedCodeBlock(parsedContent); + StoreView(blockView); + } + + public void StartAndFinalizeHtmlBlock(StringContent content) + { + var parsedContent = StringContentToStringWithLineBreaks(content); + + var blockView = ViewSupplier.GetHtmlBlock(parsedContent); + StoreView(blockView); + } + + public void StartAndFinalizeReferenceDefinitions() + { + if (_referenceDefinitions == null || _referenceDefinitions.Count == 0) + { + return; + } + + var markdownReferenceDefinition = new List(); + foreach (var referenceDefinition in _referenceDefinitions) + { + if (referenceDefinition.Value == null) + { + continue; + } + + markdownReferenceDefinition.Add(new MarkdownReferenceDefinition() + { + IsPlaceholder = referenceDefinition.Value.IsPlaceholder, + Label = referenceDefinition.Value.Label, + Title = referenceDefinition.Value.Title, + Url = referenceDefinition.Value.Url + }); + } + + var view = ViewSupplier.GetReferenceDefinitions(markdownReferenceDefinition); + StoreView(view); + } + public void StartAndFinalizeThematicBreak() { - var seperator = ViewSupplier.GetThematicBreak(); - StoreView(seperator); + var separator = ViewSupplier.GetThematicBreak(); + StoreView(separator); } public void StartAndFinalizePlaceholderBlock(string placeholderName) @@ -194,16 +254,23 @@ public void StartAndFinalizePlaceholderBlock(string placeholderName) StoreView(placeholderView); } + public string GetTextualLineBreak() + { + return ViewSupplier.GetTextualLineBreak(); + } + private T StackViews(List views) { - if (views == null || views.Count == 0) + if (views == null + || views.Count == 0) { return default(T); } // multiple views combine a single stack layout - var viewToStore = views.Count == 1 ? - views[0] : ViewSupplier.GetStackLayoutView(views); + var viewToStore = views.Count == 1 + ? views[0] + : ViewSupplier.GetStackLayoutView(views); return viewToStore; } @@ -226,6 +293,18 @@ private void StoreView(T view) WrittenViews.Add(view); } } + + private string StringContentToStringWithLineBreaks(StringContent content) + { + var stringWriter = new StringWriter(); + content.WriteTo(stringWriter); + var contentLines = stringWriter.ToString(); + + contentLines = contentLines.Replace("\r", ""); + contentLines = contentLines.TrimEnd('\n'); + contentLines = contentLines.Replace("\n", GetTextualLineBreak()); + return contentLines; + } } } diff --git a/test/MarkdownParser.Test/MarkdownParser.Test.csproj b/test/MarkdownParser.Test/MarkdownParser.Test.csproj index f93842d..9637277 100644 --- a/test/MarkdownParser.Test/MarkdownParser.Test.csproj +++ b/test/MarkdownParser.Test/MarkdownParser.Test.csproj @@ -13,20 +13,26 @@ + + + + + + diff --git a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs index 5617526..8500dba 100644 --- a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs +++ b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs @@ -98,4 +98,132 @@ public void When_parsing_nested_list_it_should_output_nesting_by_level() splittedViews[12].Should().Be("_False.2.1_textview"); splittedViews[13].Should().Be("item2-3(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(2); + + var codeViewComponentsGroup0 = parseResult[0].Split('|'); + codeViewComponentsGroup0.Length.Should().Be(4); + codeViewComponentsGroup0[0].Should().Be("fencedcodeview>"); + codeViewComponentsGroup0[1].Should().Be("(cs)"); + codeViewComponentsGroup0[2].Should().Be("var myNumber = 1;\r\nmyNumber++;"); + codeViewComponentsGroup0[3].Should().Be(""); + codeViewComponentsGroup1[1].Should().Be("the first line for IndentedCode code block\r\nthe second line for IndentedCode code block\r\nthe third line for IndentedCode code block"); + codeViewComponentsGroup1[2].Should().Be("(mockComponentSupplier); + + var newLineIndicator = mockComponentSupplier.GetTextualLineBreak(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(2); + + var htmlViewComponentsGroup0 = parseResult[0].Split('|'); + htmlViewComponentsGroup0.Length.Should().Be(3); + htmlViewComponentsGroup0.First().Should().Be("htmlview>"); + htmlViewComponentsGroup0.Last().Should().Be("First text in block

"); + firstHtmlViewContentGroup[1].Trim().Should().Be("
"); + firstHtmlViewContentGroup[2].Trim().Should().Be("

Header

"); + firstHtmlViewContentGroup[3].Trim().Should().Be("

Same block but nested element

"); + firstHtmlViewContentGroup[4].Trim().Should().Be("
"); + + var htmlViewComponentsGroup1 = parseResult[1].Split('|'); + htmlViewComponentsGroup1.Length.Should().Be(3); + htmlViewComponentsGroup1.First().Should().Be("htmlview>"); + htmlViewComponentsGroup1.Last().Should().Be(""); + secondHtmlViewContentGroup[1].Trim().Should().Be("
"); + secondHtmlViewContentGroup[2].Trim().Should().Be("

A heading

"); + secondHtmlViewContentGroup[3].Trim().Should().Be("

Posted by John Doe

"); + secondHtmlViewContentGroup[4].Trim().Should().Be("

Some additional information here

"); + secondHtmlViewContentGroup[5].Trim().Should().Be("
"); + secondHtmlViewContentGroup[6].Trim().Should().Be("

Lorem Ipsum...

"); + secondHtmlViewContentGroup[7].Trim().Should().Be(""); + } + + [TestMethod] + public void When_parsing_reference_definitions_it_should_output_specific_views() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.referencedefinitions.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(2); + parseResult[0].Should().StartWith("stackview>:+textview"); + + var referenceDefinitionsViewGroup = parseResult[1].Split('|'); + referenceDefinitionsViewGroup.Length.Should().Be(4); + referenceDefinitionsViewGroup.First().Should().Be("referencedefinitions>"); + referenceDefinitionsViewGroup.Last().Should().Be("|({codeInfo})|{content}||{content}||{content}| markdownReferenceDefinitions) + { + var content = "referencedefinitions>"; + foreach (var markdownReferenceDefinition in markdownReferenceDefinitions) + { + content += $"|{markdownReferenceDefinition.IsPlaceholder}"; + content += $"*{markdownReferenceDefinition.Label}"; + content += $"*{markdownReferenceDefinition.Title}"; + content += $"*{markdownReferenceDefinition.Url}"; + } + + content += "|First text in block

+
+

Header

+

Same block but nested element

+
+ +
+
+

A heading

+

Posted by John Doe

+

Some additional information here

+
+

Lorem Ipsum...

+
\ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/referencedefinitions.md b/test/MarkdownParser.Test/Resources/Examples/Sections/referencedefinitions.md new file mode 100644 index 0000000..75933d9 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/referencedefinitions.md @@ -0,0 +1,7 @@ +[link]: /uri "title" + +Vestibulum dictum lacinia lacus, at ornare quam consequat ultrices. +Nam quam leo, aliquet in luctus in, [porttitor non quam]. Donec tincidunt augue nisi, +sed pellentesque nisl porttitor vel. + +[porttitor non quam]: https://lipsum.com/ \ No newline at end of file