From fac6c5a200a9fcd1ac133d8618af914b1ab92842 Mon Sep 17 00:00:00 2001 From: Toine db Date: Fri, 8 Nov 2024 15:01:11 +0100 Subject: [PATCH 1/4] text to segements --- src/MarkdownParser/IViewSupplier.cs | 34 ++-- src/MarkdownParser/MarkdownParseStream.cs | 1 + src/MarkdownParser/MarkdownParser.cs | 20 +- src/MarkdownParser/MarkdownParser.csproj | 2 +- .../MarkdownReferenceDefinition.cs | 2 +- .../Models/Segments/BaseTextSegment.cs | 12 ++ .../Models/Segments/IndicatorSegment.cs | 19 ++ .../Models/Segments/LinkSegment.cs | 15 ++ .../Models/Segments/SegmentIndicator.cs | 13 ++ .../Segments/SegmentIndicatorPosition.cs | 8 + .../Models/Segments/TextSegment.cs | 18 ++ src/MarkdownParser/Models/TextBlock.cs | 25 +++ .../Models/TextSegmentHelper.cs | 34 ++++ src/MarkdownParser/ViewWriterCache.cs | 80 -------- .../{ => Writer}/ViewFormatter.cs | 18 +- src/MarkdownParser/{ => Writer}/ViewWriter.cs | 189 +++++++++++------- src/MarkdownParser/Writer/ViewWriterCache.cs | 150 ++++++++++++++ .../MarkdownParser.Test.csproj | 4 + .../MarkdownParserSectionsSpecs.cs | 31 ++- .../MarkdownParserTextSegmentsSpecs.cs | 34 ++++ .../Mocks/StringComponentSupplier.cs | 35 ++-- .../Examples/TextEmphasis/basic-links.md | 4 + .../Examples/TextEmphasis/basic-text.md | 6 + test/MarkdownParser.Test/Settings.cs | 6 + 24 files changed, 547 insertions(+), 213 deletions(-) rename src/MarkdownParser/{ => Models}/MarkdownReferenceDefinition.cs (86%) create mode 100644 src/MarkdownParser/Models/Segments/BaseTextSegment.cs create mode 100644 src/MarkdownParser/Models/Segments/IndicatorSegment.cs create mode 100644 src/MarkdownParser/Models/Segments/LinkSegment.cs create mode 100644 src/MarkdownParser/Models/Segments/SegmentIndicator.cs create mode 100644 src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs create mode 100644 src/MarkdownParser/Models/Segments/TextSegment.cs create mode 100644 src/MarkdownParser/Models/TextBlock.cs create mode 100644 src/MarkdownParser/Models/TextSegmentHelper.cs delete mode 100644 src/MarkdownParser/ViewWriterCache.cs rename src/MarkdownParser/{ => Writer}/ViewFormatter.cs (88%) rename src/MarkdownParser/{ => Writer}/ViewWriter.cs (65%) create mode 100644 src/MarkdownParser/Writer/ViewWriterCache.cs create mode 100644 test/MarkdownParser.Test/MarkdownParserTextSegmentsSpecs.cs create mode 100644 test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-links.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md create mode 100644 test/MarkdownParser.Test/Settings.cs diff --git a/src/MarkdownParser/IViewSupplier.cs b/src/MarkdownParser/IViewSupplier.cs index 522bb28..f96e58b 100644 --- a/src/MarkdownParser/IViewSupplier.cs +++ b/src/MarkdownParser/IViewSupplier.cs @@ -1,21 +1,23 @@ using System.Collections.Generic; +using MarkdownParser.Models; namespace MarkdownParser { public interface IViewSupplier { /// - /// get a textual line break + /// registering reference definitions (sometimes at the end of the document or used for creating links) /// + /// collection of Reference Definitions /// - string GetTextualLineBreak(); + void RegisterReferenceDefinitions(IEnumerable markdownReferenceDefinitions); /// /// a default text view /// - /// + /// textBlock with a collection of text segments /// - T GetTextView(string content); + T GetTextView(TextBlock textBlock); /// /// a block quote view, where other views can be inserted @@ -27,13 +29,13 @@ public interface IViewSupplier /// /// a title, subtitle or header view /// - /// - /// + /// textBlock with a collection of text segments + /// header level /// - T GetHeaderView(string content, int headerLevel); + T GetHeaderView(TextBlock textBlock, int headerLevel); /// - /// a image view with a optional subscription text view + /// a image view with an optional subscription text view /// /// image location /// (optional) null or empty when not used @@ -66,12 +68,11 @@ public interface IViewSupplier T GetStackLayoutView(List childViews); /// - /// a image view that separates content + /// an image view that separates content /// /// T GetThematicBreak(); - /// /// a placeholder for views or other objects /// @@ -83,25 +84,18 @@ public interface IViewSupplier /// a view that shows fenced code (found in MD blocks starting with ```cs ) /// /// - T GetFencedCodeBlock(string content, string codeInfo); + T GetFencedCodeBlock(TextBlock textBlock, string codeInfo); /// /// a view that shows indented code (found in MD lines starting with at least 4 spaces) /// /// - T GetIndentedCodeBlock(string content); + T GetIndentedCodeBlock(TextBlock textBlock); /// /// 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); + T GetHtmlBlock(TextBlock textBlock); } } diff --git a/src/MarkdownParser/MarkdownParseStream.cs b/src/MarkdownParser/MarkdownParseStream.cs index 005cb4b..a70cc9d 100644 --- a/src/MarkdownParser/MarkdownParseStream.cs +++ b/src/MarkdownParser/MarkdownParseStream.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using CommonMark.Syntax; +using MarkdownParser.Writer; namespace MarkdownParser { diff --git a/src/MarkdownParser/MarkdownParser.cs b/src/MarkdownParser/MarkdownParser.cs index 4a3d25b..603a472 100644 --- a/src/MarkdownParser/MarkdownParser.cs +++ b/src/MarkdownParser/MarkdownParser.cs @@ -2,6 +2,7 @@ using System.IO; using CommonMark; using CommonMark.Syntax; +using MarkdownParser.Writer; namespace MarkdownParser { @@ -14,6 +15,11 @@ public MarkdownParser(IViewSupplier viewSupplier) _viewSupplier = viewSupplier; } + /// + /// Format\Convert markdown into UI Components + /// + /// + /// public List Parse(string markdownSource) { using (var reader = new StringReader(markdownSource)) @@ -22,6 +28,11 @@ public List Parse(string markdownSource) } } + /// + /// Format\Convert markdown into UI Components + /// + /// + /// public List Parse(TextReader markdownSource) { // Parse to usable c# objects @@ -34,11 +45,18 @@ public List Parse(TextReader markdownSource) return uiComponents; } - public static Block GetMarkdownDocument(TextReader markdownSource) + /// + /// Get usable c# objects from + /// s + /// + /// + internal static Block GetMarkdownDocument(TextReader markdownSource) { // Parse to usable c# objects var settings = CommonMarkSettings.Default.Clone(); settings.AdditionalFeatures |= CommonMarkAdditionalFeatures.PlaceholderBracket; + settings.AdditionalFeatures |= CommonMarkAdditionalFeatures.StrikethroughTilde; + return CommonMarkConverter.Parse(markdownSource, settings); } } diff --git a/src/MarkdownParser/MarkdownParser.csproj b/src/MarkdownParser/MarkdownParser.csproj index 5e485e8..091b49a 100644 --- a/src/MarkdownParser/MarkdownParser.csproj +++ b/src/MarkdownParser/MarkdownParser.csproj @@ -46,7 +46,7 @@ - + diff --git a/src/MarkdownParser/MarkdownReferenceDefinition.cs b/src/MarkdownParser/Models/MarkdownReferenceDefinition.cs similarity index 86% rename from src/MarkdownParser/MarkdownReferenceDefinition.cs rename to src/MarkdownParser/Models/MarkdownReferenceDefinition.cs index 5ab3892..2c9d50c 100644 --- a/src/MarkdownParser/MarkdownReferenceDefinition.cs +++ b/src/MarkdownParser/Models/MarkdownReferenceDefinition.cs @@ -1,4 +1,4 @@ -namespace MarkdownParser +namespace MarkdownParser.Models { public class MarkdownReferenceDefinition { diff --git a/src/MarkdownParser/Models/Segments/BaseTextSegment.cs b/src/MarkdownParser/Models/Segments/BaseTextSegment.cs new file mode 100644 index 0000000..f25c30d --- /dev/null +++ b/src/MarkdownParser/Models/Segments/BaseTextSegment.cs @@ -0,0 +1,12 @@ +namespace MarkdownParser.Models.Segments +{ + public abstract class BaseSegment + { + public bool HasLiteralContent { get; protected set; } = false; + + public override string ToString() + { + return string.Empty; + } + } +} diff --git a/src/MarkdownParser/Models/Segments/IndicatorSegment.cs b/src/MarkdownParser/Models/Segments/IndicatorSegment.cs new file mode 100644 index 0000000..d717470 --- /dev/null +++ b/src/MarkdownParser/Models/Segments/IndicatorSegment.cs @@ -0,0 +1,19 @@ +namespace MarkdownParser.Models.Segments +{ + public class IndicatorSegment : BaseSegment + { + public SegmentIndicator Indicator { get; } + public SegmentIndicatorPosition IndicatorPosition { get; } + + public IndicatorSegment(SegmentIndicator indicator, SegmentIndicatorPosition indicatorPosition) + { + Indicator = indicator; + IndicatorPosition = indicatorPosition; + } + + public override string ToString() + { + return string.Empty; + } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/Segments/LinkSegment.cs b/src/MarkdownParser/Models/Segments/LinkSegment.cs new file mode 100644 index 0000000..40c5ae9 --- /dev/null +++ b/src/MarkdownParser/Models/Segments/LinkSegment.cs @@ -0,0 +1,15 @@ +namespace MarkdownParser.Models.Segments +{ + public class LinkSegment : IndicatorSegment + { + public string Url { get; } + public string Title { get; } + + public LinkSegment(SegmentIndicator indicator, SegmentIndicatorPosition indicatorPosition, string url, string title) + : base(indicator, indicatorPosition) + { + Url = url; + Title = title; + } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/Segments/SegmentIndicator.cs b/src/MarkdownParser/Models/Segments/SegmentIndicator.cs new file mode 100644 index 0000000..97e8861 --- /dev/null +++ b/src/MarkdownParser/Models/Segments/SegmentIndicator.cs @@ -0,0 +1,13 @@ +namespace MarkdownParser.Models.Segments +{ + public enum SegmentIndicator + { + NotSupported, + Strong, + Italic, + Strikethrough, + Code, + Link, + LineBreak + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs b/src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs new file mode 100644 index 0000000..5ccaab3 --- /dev/null +++ b/src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs @@ -0,0 +1,8 @@ +namespace MarkdownParser.Models.Segments +{ + public enum SegmentIndicatorPosition + { + Start, + End + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/Segments/TextSegment.cs b/src/MarkdownParser/Models/Segments/TextSegment.cs new file mode 100644 index 0000000..2e98914 --- /dev/null +++ b/src/MarkdownParser/Models/Segments/TextSegment.cs @@ -0,0 +1,18 @@ +namespace MarkdownParser.Models.Segments +{ + public sealed class Segment : BaseSegment + { + public string Text { get; set; } + + public Segment(string text) + { + Text = text; + HasLiteralContent = !string.IsNullOrWhiteSpace(Text); + } + + public override string ToString() + { + return Text ?? string.Empty; + } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/TextBlock.cs b/src/MarkdownParser/Models/TextBlock.cs new file mode 100644 index 0000000..a94e8da --- /dev/null +++ b/src/MarkdownParser/Models/TextBlock.cs @@ -0,0 +1,25 @@ +using MarkdownParser.Models.Segments; + +namespace MarkdownParser.Models +{ + public class TextBlock + { + public BaseSegment[] TextSegments { get; } + + public TextBlock(BaseSegment[] textSegments) + { + TextSegments = textSegments; + } + + /// + /// Get all text (LiteralContent) and ignoring all emphasize + /// + /// text used for line breaks + /// clean text + public string ExtractLiteralContent(string usedStringForLineBreaks) + { + var literalContent = TextSegmentHelper.TextSegmentsToLiteralContent(TextSegments, usedStringForLineBreaks); + return literalContent; + } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/TextSegmentHelper.cs b/src/MarkdownParser/Models/TextSegmentHelper.cs new file mode 100644 index 0000000..9286386 --- /dev/null +++ b/src/MarkdownParser/Models/TextSegmentHelper.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Text; +using MarkdownParser.Models.Segments; + +namespace MarkdownParser.Models +{ + public static class TextSegmentHelper + { + public static string TextSegmentsToLiteralContent(BaseSegment[] segments, string optionalLineBreak) + { + var literalContentBuilder = new StringBuilder(); + + if (segments == null + || !segments.Any(x => x.HasLiteralContent)) + { + return string.Empty; + } + + for (var i = 0; i < segments.Length; i++) + { + var literalContent = segments[i].ToString(); + if (segments[i] is IndicatorSegment indicatorSegment + && indicatorSegment.Indicator == SegmentIndicator.LineBreak) + { + literalContent = optionalLineBreak; + } + + literalContentBuilder.Append(literalContent); + } + + return literalContentBuilder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/ViewWriterCache.cs b/src/MarkdownParser/ViewWriterCache.cs deleted file mode 100644 index 4f1ce4f..0000000 --- a/src/MarkdownParser/ViewWriterCache.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using CommonMark.Syntax; - -namespace MarkdownParser -{ - public class ViewWriterCache - { - public BlockTag? ComponentType { get; set; } - - private readonly Stack> _valuesStack = new Stack>(); - private readonly T _defaultT = default(T); - - public void Add(T item) - { - if (EqualityComparer.Default.Equals(item, _defaultT)) - { - return; - } - - _valuesStack.Push(new Tuple(null, item)); - } - - public void Add(string item) - { - if (string.IsNullOrEmpty(item)) - { - return; - } - - _valuesStack.Push(new Tuple(item, _defaultT)); - } - - /// - /// Get cached items in order, each Tuple has a text or T value (never both in the same Tuple) - /// - /// collection that contain a string of T (never both in the same Tuple) - public List> GetGroupedCachedValues() - { - var groupedCache = new List>(); - - var workbenchItemTextCacheBuilder = new StringBuilder(); - var values = _valuesStack.Reverse().ToArray(); - - foreach (var value in values) - { - // Check for text - if (!string.IsNullOrEmpty(value.Item1)) - { - workbenchItemTextCacheBuilder.Append(value.Item1); - continue; - } - - // No text anymore: Store workbenchItemTextCache if any to groupedCache - if (workbenchItemTextCacheBuilder.Length != 0) - { - groupedCache.Add(new Tuple(workbenchItemTextCacheBuilder.ToString(), _defaultT)); - workbenchItemTextCacheBuilder.Clear(); - } - - // If item2 is not null - if (!EqualityComparer.Default.Equals(value.Item2, _defaultT)) - { - groupedCache.Add(new Tuple(null, value.Item2)); - } - } - - // Store leftovers workbenchItemTextCache - if (workbenchItemTextCacheBuilder.Length != 0) - { - groupedCache.Add(new Tuple(workbenchItemTextCacheBuilder.ToString(), _defaultT)); - workbenchItemTextCacheBuilder.Clear(); - } - - return groupedCache; - } - } -} \ No newline at end of file diff --git a/src/MarkdownParser/ViewFormatter.cs b/src/MarkdownParser/Writer/ViewFormatter.cs similarity index 88% rename from src/MarkdownParser/ViewFormatter.cs rename to src/MarkdownParser/Writer/ViewFormatter.cs index 851e480..c847133 100644 --- a/src/MarkdownParser/ViewFormatter.cs +++ b/src/MarkdownParser/Writer/ViewFormatter.cs @@ -3,7 +3,7 @@ using CommonMark; using CommonMark.Syntax; -namespace MarkdownParser +namespace MarkdownParser.Writer { public class ViewFormatter { @@ -17,7 +17,6 @@ public ViewFormatter(IViewSupplier viewSupplier) public List Format(Block markdownBlock) { WriteBlockToView(markdownBlock, _writer); - _writer.StartAndFinalizeReferenceDefinitions(); return _writer.Flush(); } @@ -82,7 +81,7 @@ private void WriteBlockToView(Block block, ViewWriter writer, bool continueWi writer.StartAndFinalizeHtmlBlock(block.StringContent); break; case BlockTag.ReferenceDefinition: - // ignore, handled at the end of document by _writer.StartAndFinalizeReferenceDefinitions() + // ignore this tag, because it's handled before so the data can be used during the formatting break; default: throw new CommonMarkException("Block type " + block.Tag + " is not supported.", block); @@ -103,13 +102,16 @@ private void WriteInlineToView(Inline inline, ViewWriter writer) switch (inline.Tag) { - case InlineTag.String: case InlineTag.Code: + writer.AddEmphasis(inline, inline.SourcePosition, inline.SourceLength); + writer.AddText(inline.LiteralContent, inline.SourcePosition); + break; + case InlineTag.String: case InlineTag.RawHtml: - writer.AddText(inline.LiteralContent); + writer.AddText(inline.LiteralContent, inline.SourcePosition); break; case InlineTag.Link: - // TODO; maybe + writer.AddLink(inline, inline.SourcePosition, inline.SourceLength, inline.TargetUrl, inline.LiteralContent); // check if this works at links WriteInlineToView(inline.FirstChild, writer); break; case InlineTag.Image: @@ -117,7 +119,7 @@ private void WriteInlineToView(Inline inline, ViewWriter writer) break; case InlineTag.SoftBreak: case InlineTag.LineBreak: - writer.AddText(writer.GetTextualLineBreak()); + writer.AddEmphasis(inline, inline.SourcePosition, inline.SourceLength); break; case InlineTag.Placeholder: writer.StartAndFinalizePlaceholderBlock(inline.TargetUrl); @@ -125,9 +127,9 @@ private void WriteInlineToView(Inline inline, ViewWriter writer) case InlineTag.Strikethrough: case InlineTag.Emphasis: case InlineTag.Strong: + writer.AddEmphasis(inline, inline.SourcePosition, inline.SourceLength); WriteInlineToView(inline.FirstChild, writer); break; - default: throw new ArgumentOutOfRangeException(); } diff --git a/src/MarkdownParser/ViewWriter.cs b/src/MarkdownParser/Writer/ViewWriter.cs similarity index 65% rename from src/MarkdownParser/ViewWriter.cs rename to src/MarkdownParser/Writer/ViewWriter.cs index 2578803..73dd695 100644 --- a/src/MarkdownParser/ViewWriter.cs +++ b/src/MarkdownParser/Writer/ViewWriter.cs @@ -3,8 +3,10 @@ using System.IO; using System.Linq; using CommonMark.Syntax; +using MarkdownParser.Models; +using MarkdownParser.Models.Segments; -namespace MarkdownParser +namespace MarkdownParser.Writer { public class ViewWriter { @@ -39,12 +41,35 @@ public List Flush() public void RegisterReferenceDefinitions(Dictionary referenceDefinitions) { _referenceDefinitions = referenceDefinitions; + + 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 + }); + } + + ViewSupplier.RegisterReferenceDefinitions(markdownReferenceDefinition); } - public void StartBlock(BlockTag blockType, string content = "") + public void StartBlock(BlockTag blockType) { Workbench.Push(new ViewWriterCache { ComponentType = blockType }); - GetWorkbenchItem().Add(content); } public void FinalizeParagraphBlock() @@ -59,17 +84,17 @@ public void FinalizeParagraphBlock() var views = new List(); var topWorkbenchItem = Workbench.Pop(); - var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + var itemsCache = topWorkbenchItem.FlushCache(); foreach (var itemsCacheTuple in itemsCache) { - var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) - ? ViewSupplier.GetTextView(itemsCacheTuple.Item1) - : itemsCacheTuple.Item2; + var view = itemsCacheTuple.TextBlock != null + ? ViewSupplier.GetTextView(itemsCacheTuple.TextBlock) + : itemsCacheTuple.Value; if (view != null) { - views.Add(view); + views.Add(view); } } @@ -86,7 +111,7 @@ public void FinalizeBlockquoteBlock() } var topWorkbenchItem = Workbench.Pop(); - var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + var itemsCache = topWorkbenchItem.FlushCache(); var childViews = itemsCache.Select(itemsCacheTuple => itemsCacheTuple.Item2).ToList(); var childView = StackViews(childViews); @@ -109,13 +134,13 @@ public void FinalizeHeaderBlock(int headerLevel) var views = new List(); var topWorkbenchItem = Workbench.Pop(); - var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + var itemsCache = topWorkbenchItem.FlushCache(); foreach (var itemsCacheTuple in itemsCache) { - var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) - ? ViewSupplier.GetHeaderView(itemsCacheTuple.Item1, headerLevel) - : itemsCacheTuple.Item2; + var view = itemsCacheTuple.TextBlock != null + ? ViewSupplier.GetHeaderView(itemsCacheTuple.TextBlock, headerLevel) + : itemsCacheTuple.Value; views.Add(view); } @@ -133,7 +158,7 @@ public void FinalizeListBlock() } var topWorkbenchItem = Workbench.Pop(); - var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + var itemsCache = topWorkbenchItem.FlushCache(); var listItems = itemsCache.Select(itemsCacheTuple => itemsCacheTuple.Item2).ToList(); var listView = ViewSupplier.GetListView(listItems); @@ -157,18 +182,17 @@ public void FinalizeListItemBlock(ListData listData) var depthLevel = Workbench.Count(wbItem => wbItem.ComponentType == BlockTag.List); var topWorkbenchItem = Workbench.Pop(); - var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); - + var itemsCache = topWorkbenchItem.FlushCache(); foreach (var itemsCacheTuple in itemsCache) { - var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) - ? ViewSupplier.GetTextView(itemsCacheTuple.Item1) - : itemsCacheTuple.Item2; + var view = itemsCacheTuple.TextBlock != null + ? ViewSupplier.GetTextView(itemsCacheTuple.TextBlock) + : itemsCacheTuple.Value; if (view != null) { - views.Add(view); + views.Add(view); } } @@ -179,9 +203,48 @@ public void FinalizeListItemBlock(ListData listData) StoreView(listItemView); } - public void AddText(string content) + public void AddText(string content, int firstCharacterPosition) { - GetWorkbenchItem().Add(content); + GetWorkbenchItem().Add(content, firstCharacterPosition); + } + + public void AddLink(Inline inline, int firstCharacterPosition, int length, string url, string urlTitle) + { + GetWorkbenchItem().AddLink(firstCharacterPosition, length, url, urlTitle); + } + + public void AddEmphasis(Inline inline, int firstCharacterPosition, int length) + { + SegmentIndicator indicator; + switch (inline.Tag) + { + case InlineTag.Strikethrough: + indicator = SegmentIndicator.Strikethrough; + break; + case InlineTag.Strong: + indicator = SegmentIndicator.Strong; + break; + case InlineTag.Code: + indicator = SegmentIndicator.Code; + break; + case InlineTag.Emphasis: + indicator = SegmentIndicator.Italic; + break; + case InlineTag.LineBreak: + case InlineTag.SoftBreak: + indicator = SegmentIndicator.LineBreak; + break; + default: + indicator = SegmentIndicator.NotSupported; + break; + } + + if (indicator == SegmentIndicator.NotSupported) + { + return; + } + + GetWorkbenchItem().Add(indicator, firstCharacterPosition, length); } public void StartAndFinalizeImageBlock(string targetUrl, string subscription, string imageId) @@ -192,56 +255,28 @@ public void StartAndFinalizeImageBlock(string targetUrl, string subscription, st public void StartAndFinalizeFencedCodeBlock(StringContent content, string blockInfo) { - var parsedContent = StringContentToStringWithLineBreaks(content); + var blocks = StringContentToBlocks(content); - var blockView = ViewSupplier.GetFencedCodeBlock(parsedContent, blockInfo); + var blockView = ViewSupplier.GetFencedCodeBlock(blocks, blockInfo); StoreView(blockView); } public void StartAndFinalizeIndentedCodeBlock(StringContent content) { - var parsedContent = StringContentToStringWithLineBreaks(content); + var blocks = StringContentToBlocks(content); - var blockView = ViewSupplier.GetIndentedCodeBlock(parsedContent); + var blockView = ViewSupplier.GetIndentedCodeBlock(blocks); StoreView(blockView); } - + public void StartAndFinalizeHtmlBlock(StringContent content) { - var parsedContent = StringContentToStringWithLineBreaks(content); + var blocks = StringContentToBlocks(content); - var blockView = ViewSupplier.GetHtmlBlock(parsedContent); + var blockView = ViewSupplier.GetHtmlBlock(blocks); 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 separator = ViewSupplier.GetThematicBreak(); @@ -254,22 +289,17 @@ public void StartAndFinalizePlaceholderBlock(string placeholderName) StoreView(placeholderView); } - public string GetTextualLineBreak() - { - return ViewSupplier.GetTextualLineBreak(); - } - private T StackViews(List views) { - if (views == null + if (views == null || views.Count == 0) { - return default(T); + return default; } // multiple views combine a single stack layout - var viewToStore = views.Count == 1 - ? views[0] + var viewToStore = views.Count == 1 + ? views[0] : ViewSupplier.GetStackLayoutView(views); return viewToStore; @@ -282,7 +312,7 @@ private void StoreView(T view) return; } - // Check if Workbench has an item where its working on + // Check if Workbench has an item where it's working on var wbi = GetWorkbenchItem(); if (wbi != null) // add the new View to the WorkbenchItem { @@ -293,8 +323,8 @@ private void StoreView(T view) WrittenViews.Add(view); } } - - private string StringContentToStringWithLineBreaks(StringContent content) + + private TextBlock StringContentToBlocks(StringContent content) { var stringWriter = new StringWriter(); content.WriteTo(stringWriter); @@ -302,9 +332,26 @@ private string StringContentToStringWithLineBreaks(StringContent content) contentLines = contentLines.Replace("\r", ""); contentLines = contentLines.TrimEnd('\n'); - contentLines = contentLines.Replace("\n", GetTextualLineBreak()); - return contentLines; + var contentParts = contentLines.Split('\n'); + var segments = new List(); + if (contentParts.Any()) + { + segments.Add(new Segment(contentParts.First())); + + for (var i = 1; i < contentParts.Length; i++) + { + var lineBreakSegment = new IndicatorSegment(SegmentIndicator.LineBreak, SegmentIndicatorPosition.Start); + segments.Add(lineBreakSegment); + + var textSegment = new Segment(contentParts[i]); + segments.Add(textSegment); + } + } + + var textBlock = new TextBlock(segments.ToArray()); + + return textBlock; } } } diff --git a/src/MarkdownParser/Writer/ViewWriterCache.cs b/src/MarkdownParser/Writer/ViewWriterCache.cs new file mode 100644 index 0000000..7e43367 --- /dev/null +++ b/src/MarkdownParser/Writer/ViewWriterCache.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Linq; +using CommonMark.Syntax; +using MarkdownParser.Models; +using MarkdownParser.Models.Segments; + +namespace MarkdownParser.Writer +{ + public class ViewWriterCache + { + public BlockTag? ComponentType { get; set; } + + // Stack to use as build or cache collection + private readonly Stack<(BaseSegment Segment, T Value)> _valuesStack = new Stack<(BaseSegment, T)>(); + + // SegmentIndicators that are not ended/closed yet + private readonly List<(Models.Segments.SegmentIndicator SegmentIndicator, int lastCharacterPosition)> _pendingSegmentIndicators = new List<(SegmentIndicator SegmentIndicator, int lastCharacterPosition)>(); + + private readonly T _defaultT = default; + + public void Add(T item) + { + if (EqualityComparer.Default.Equals(item, _defaultT)) + { + return; + } + + _valuesStack.Push((null, item)); + } + + public void Add(string item, int firstCharacterPosition) + { + FinalizeSegmentIndicator(firstCharacterPosition); + + if (string.IsNullOrEmpty(item)) + { + return; + } + + var segment = new Segment(item); + _valuesStack.Push((segment, _defaultT)); + } + + public void AddLink(int firstCharacterPosition, int length, string url, string urlTitle) + { + FinalizeSegmentIndicator(firstCharacterPosition); + + _pendingSegmentIndicators.Add((SegmentIndicator.Link, firstCharacterPosition + length - 1)); + + var segmentIndicator = new LinkSegment(SegmentIndicator.Link, SegmentIndicatorPosition.Start, url, urlTitle); + _valuesStack.Push((segmentIndicator, _defaultT)); + } + + public void Add(SegmentIndicator indicator, int firstCharacterPosition, int length) + { + FinalizeSegmentIndicator(firstCharacterPosition); + + if (indicator != SegmentIndicator.LineBreak) + { + _pendingSegmentIndicators.Add((indicator, firstCharacterPosition + length - 1)); + } + + var segmentIndicator = new IndicatorSegment(indicator, SegmentIndicatorPosition.Start); + _valuesStack.Push((segmentIndicator, _defaultT)); + } + + /// + /// Get cached items in order, each Tuple has a text or T value (never both in the same Tuple) + /// + /// collection that contain TextSegments or T (never both in the same Tuple) + public List<(TextBlock TextBlock, T Value)> FlushCache() + { + FinalizeRemainingSegmentIndicators(); + + var groupedCache = new List<(TextBlock, T)>(); + + var textSegmentsCache = new List(); + var values = _valuesStack.Reverse().ToArray(); + + foreach (var value in values) + { + // Check for text + if (value.Segment != null) + { + textSegmentsCache.Add(value.Segment); + continue; + } + + // No text anymore: Store workbenchItemTextCache if any to groupedCache + if (textSegmentsCache.Any()) + { + groupedCache.Add((new TextBlock(textSegmentsCache.ToArray()), _defaultT)); + textSegmentsCache.Clear(); + } + + // If item2 is not null + if (!EqualityComparer.Default.Equals(value.Item2, _defaultT)) + { + groupedCache.Add((null, value.Item2)); + } + } + + // Store leftovers workbenchItemTextCache + if (textSegmentsCache.Any()) + { + groupedCache.Add((new TextBlock(textSegmentsCache.ToArray()), _defaultT)); + textSegmentsCache.Clear(); + } + + return groupedCache; + } + + private void FinalizeSegmentIndicator(int toWritePosition) + { + if (!_pendingSegmentIndicators.Any()) + { + return; + } + + var pendingIndicators = _pendingSegmentIndicators.ToArray(); + foreach (var pendingIndicator in pendingIndicators) + { + if (toWritePosition > pendingIndicator.lastCharacterPosition) + { + _pendingSegmentIndicators.Remove(pendingIndicator); + + var segmentIndicator = new IndicatorSegment(pendingIndicator.SegmentIndicator, SegmentIndicatorPosition.End); + _valuesStack.Push((segmentIndicator, _defaultT)); + } + } + } + + private void FinalizeRemainingSegmentIndicators() + { + if (!_pendingSegmentIndicators.Any()) + { + return; + } + + var pendingSegmentIndicators = _pendingSegmentIndicators.OrderBy(x => x.lastCharacterPosition); + foreach (var pendingSegmentIndicator in pendingSegmentIndicators) + { + var segmentIndicator = new IndicatorSegment(pendingSegmentIndicator.SegmentIndicator, SegmentIndicatorPosition.End); + _valuesStack.Push((segmentIndicator, _defaultT)); + } + + _pendingSegmentIndicators.Clear(); + } + } +} \ No newline at end of file diff --git a/test/MarkdownParser.Test/MarkdownParser.Test.csproj b/test/MarkdownParser.Test/MarkdownParser.Test.csproj index 9637277..1391f1b 100644 --- a/test/MarkdownParser.Test/MarkdownParser.Test.csproj +++ b/test/MarkdownParser.Test/MarkdownParser.Test.csproj @@ -20,6 +20,8 @@ + + @@ -33,6 +35,8 @@ + + diff --git a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs index 8500dba..492fac7 100644 --- a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs +++ b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs @@ -145,7 +145,7 @@ public void When_parsing_htmlblocks_it_should_output_html_views() var mockComponentSupplier = new StringComponentSupplier(); var parser = new MarkdownParser(mockComponentSupplier); - var newLineIndicator = mockComponentSupplier.GetTextualLineBreak(); + var newLineIndicator = Settings.TextualLineBreak; //----------------------------------------------------------------------------------------------------------- // Act @@ -206,24 +206,19 @@ public void When_parsing_reference_definitions_it_should_output_specific_views() //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - parseResult.Count.Should().Be(2); + parseResult.Count.Should().Be(1); 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("(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + //parseResult.Count.Should().Be(3); + //parseResult[0].Should().StartWith("textview:Paragraphs are"); + //parseResult[1].Should().StartWith("textview:2nd paragraph."); + //parseResult[2].Should().StartWith("textview:Note that"); + } +} \ No newline at end of file diff --git a/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs b/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs index 6711785..6cfdaad 100644 --- a/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs +++ b/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs @@ -1,9 +1,19 @@ -namespace MarkdownParser.Test.Mocks; +using MarkdownParser.Models; + +namespace MarkdownParser.Test.Mocks; internal class StringComponentSupplier : IViewSupplier { - public string GetTextView(string content) + public MarkdownReferenceDefinition[]? MarkdownReferenceDefinitions { get; private set; } + + public void RegisterReferenceDefinitions(IEnumerable markdownReferenceDefinitions) + { + MarkdownReferenceDefinitions = markdownReferenceDefinitions.ToArray(); + } + + public string GetTextView(TextBlock textBlock) { + var content = textBlock.ExtractLiteralContent(Settings.TextualLineBreak); return $"textview:{content}"; } @@ -12,8 +22,9 @@ public string GetBlockquotesView(string content) return $"blockquoteview>:{content}|({codeInfo})|{content}||{content}||{content}| markdownReferenceDefinitions) + public string GetReferenceDefinitions() { var content = "referencedefinitions>"; - foreach (var markdownReferenceDefinition in markdownReferenceDefinitions) + foreach (var markdownReferenceDefinition in MarkdownReferenceDefinitions) { content += $"|{markdownReferenceDefinition.IsPlaceholder}"; content += $"*{markdownReferenceDefinition.Label}"; @@ -82,9 +96,4 @@ public string GetReferenceDefinitions(IEnumerable m return content; } - - public string GetTextualLineBreak() - { - return Environment.NewLine; - } } diff --git a/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-links.md b/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-links.md new file mode 100644 index 0000000..c2e0218 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-links.md @@ -0,0 +1,4 @@ +My favorite search engine is [Duck Duck Go](https://duckduckgo.com "The best search engine for privacy"). + + + \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md b/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md new file mode 100644 index 0000000..3d559e1 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md @@ -0,0 +1,6 @@ +Use ~~double~~ tildes around the words. + +Paragraphs **are *separated* by** a blank line. + +2nd paragraph. *this is Italic*, **this is bold**, and `monospace`. Itemized lists +look like: diff --git a/test/MarkdownParser.Test/Settings.cs b/test/MarkdownParser.Test/Settings.cs new file mode 100644 index 0000000..135b155 --- /dev/null +++ b/test/MarkdownParser.Test/Settings.cs @@ -0,0 +1,6 @@ +namespace MarkdownParser.Test; + +internal static class Settings +{ + public static string TextualLineBreak { get; } = Environment.NewLine; +} From 2f44fb6571e07cfa845d849db78c06780e8cdf3e Mon Sep 17 00:00:00 2001 From: Toine db Date: Tue, 12 Nov 2024 22:38:19 +0100 Subject: [PATCH 2/4] .net 9 --- src/MarkdownParser/IViewSupplier.cs | 4 +- src/MarkdownParser/MarkdownParser.csproj | 5 +- .../Models/Segments/IndicatorSegment.cs | 4 +- .../{ => Indicators}/SegmentIndicator.cs | 2 +- .../SegmentIndicatorPosition.cs | 2 +- .../Models/Segments/LinkSegment.cs | 4 +- .../Models/Segments/TextSegment.cs | 4 +- .../Models/TextSegmentHelper.cs | 1 + src/MarkdownParser/Writer/ViewWriter.cs | 5 +- src/MarkdownParser/Writer/ViewWriterCache.cs | 17 ++- .../MarkdownParser.Test.csproj | 2 +- .../MarkdownParserBlockSegmentsSpecs.cs | 139 ++++++++++++++++++ .../MarkdownParserTextSegmentsSpecs.cs | 38 ++++- .../Mocks/PassThroughComponentSupplier.cs | 73 +++++++++ .../Examples/TextEmphasis/basic-text.md | 2 +- 15 files changed, 278 insertions(+), 24 deletions(-) rename src/MarkdownParser/Models/Segments/{ => Indicators}/SegmentIndicator.cs (75%) rename src/MarkdownParser/Models/Segments/{ => Indicators}/SegmentIndicatorPosition.cs (60%) create mode 100644 test/MarkdownParser.Test/MarkdownParserBlockSegmentsSpecs.cs create mode 100644 test/MarkdownParser.Test/Mocks/PassThroughComponentSupplier.cs diff --git a/src/MarkdownParser/IViewSupplier.cs b/src/MarkdownParser/IViewSupplier.cs index f96e58b..65b0715 100644 --- a/src/MarkdownParser/IViewSupplier.cs +++ b/src/MarkdownParser/IViewSupplier.cs @@ -44,7 +44,7 @@ public interface IViewSupplier T GetImageView(string url, string subscription, string imageId); /// - /// a view that shows a list of listitems + /// a view that shows a list of list-items /// /// /// @@ -53,7 +53,7 @@ public interface IViewSupplier /// /// a view that shows a single item for a ListView (return View can be used in GetListView) /// - /// view as childview (or use the content parameter) + /// view as child-view (or use the content parameter) /// does the item belong to a ordered (numbered) list /// number of sequence /// level depth of the list, root level starting at 1 diff --git a/src/MarkdownParser/MarkdownParser.csproj b/src/MarkdownParser/MarkdownParser.csproj index 091b49a..2d787c4 100644 --- a/src/MarkdownParser/MarkdownParser.csproj +++ b/src/MarkdownParser/MarkdownParser.csproj @@ -2,8 +2,9 @@ - netstandard2.0;net8.0 - + netstandard2.0;net8.0;net9.0 + true + Toine de Boer Copyright © Toine de Boer and contributors diff --git a/src/MarkdownParser/Models/Segments/IndicatorSegment.cs b/src/MarkdownParser/Models/Segments/IndicatorSegment.cs index d717470..df83560 100644 --- a/src/MarkdownParser/Models/Segments/IndicatorSegment.cs +++ b/src/MarkdownParser/Models/Segments/IndicatorSegment.cs @@ -1,4 +1,6 @@ -namespace MarkdownParser.Models.Segments +using MarkdownParser.Models.Segments.Indicators; + +namespace MarkdownParser.Models.Segments { public class IndicatorSegment : BaseSegment { diff --git a/src/MarkdownParser/Models/Segments/SegmentIndicator.cs b/src/MarkdownParser/Models/Segments/Indicators/SegmentIndicator.cs similarity index 75% rename from src/MarkdownParser/Models/Segments/SegmentIndicator.cs rename to src/MarkdownParser/Models/Segments/Indicators/SegmentIndicator.cs index 97e8861..53583f2 100644 --- a/src/MarkdownParser/Models/Segments/SegmentIndicator.cs +++ b/src/MarkdownParser/Models/Segments/Indicators/SegmentIndicator.cs @@ -1,4 +1,4 @@ -namespace MarkdownParser.Models.Segments +namespace MarkdownParser.Models.Segments.Indicators { public enum SegmentIndicator { diff --git a/src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs b/src/MarkdownParser/Models/Segments/Indicators/SegmentIndicatorPosition.cs similarity index 60% rename from src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs rename to src/MarkdownParser/Models/Segments/Indicators/SegmentIndicatorPosition.cs index 5ccaab3..56760fb 100644 --- a/src/MarkdownParser/Models/Segments/SegmentIndicatorPosition.cs +++ b/src/MarkdownParser/Models/Segments/Indicators/SegmentIndicatorPosition.cs @@ -1,4 +1,4 @@ -namespace MarkdownParser.Models.Segments +namespace MarkdownParser.Models.Segments.Indicators { public enum SegmentIndicatorPosition { diff --git a/src/MarkdownParser/Models/Segments/LinkSegment.cs b/src/MarkdownParser/Models/Segments/LinkSegment.cs index 40c5ae9..5a2a5f3 100644 --- a/src/MarkdownParser/Models/Segments/LinkSegment.cs +++ b/src/MarkdownParser/Models/Segments/LinkSegment.cs @@ -1,4 +1,6 @@ -namespace MarkdownParser.Models.Segments +using MarkdownParser.Models.Segments.Indicators; + +namespace MarkdownParser.Models.Segments { public class LinkSegment : IndicatorSegment { diff --git a/src/MarkdownParser/Models/Segments/TextSegment.cs b/src/MarkdownParser/Models/Segments/TextSegment.cs index 2e98914..d758a5c 100644 --- a/src/MarkdownParser/Models/Segments/TextSegment.cs +++ b/src/MarkdownParser/Models/Segments/TextSegment.cs @@ -1,10 +1,10 @@ namespace MarkdownParser.Models.Segments { - public sealed class Segment : BaseSegment + public class TextSegment : BaseSegment { public string Text { get; set; } - public Segment(string text) + public TextSegment(string text) { Text = text; HasLiteralContent = !string.IsNullOrWhiteSpace(Text); diff --git a/src/MarkdownParser/Models/TextSegmentHelper.cs b/src/MarkdownParser/Models/TextSegmentHelper.cs index 9286386..bca71ef 100644 --- a/src/MarkdownParser/Models/TextSegmentHelper.cs +++ b/src/MarkdownParser/Models/TextSegmentHelper.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Text; using MarkdownParser.Models.Segments; +using MarkdownParser.Models.Segments.Indicators; namespace MarkdownParser.Models { diff --git a/src/MarkdownParser/Writer/ViewWriter.cs b/src/MarkdownParser/Writer/ViewWriter.cs index 73dd695..9a520a0 100644 --- a/src/MarkdownParser/Writer/ViewWriter.cs +++ b/src/MarkdownParser/Writer/ViewWriter.cs @@ -5,6 +5,7 @@ using CommonMark.Syntax; using MarkdownParser.Models; using MarkdownParser.Models.Segments; +using MarkdownParser.Models.Segments.Indicators; namespace MarkdownParser.Writer { @@ -337,14 +338,14 @@ private TextBlock StringContentToBlocks(StringContent content) var segments = new List(); if (contentParts.Any()) { - segments.Add(new Segment(contentParts.First())); + segments.Add(new TextSegment(contentParts.First())); for (var i = 1; i < contentParts.Length; i++) { var lineBreakSegment = new IndicatorSegment(SegmentIndicator.LineBreak, SegmentIndicatorPosition.Start); segments.Add(lineBreakSegment); - var textSegment = new Segment(contentParts[i]); + var textSegment = new TextSegment(contentParts[i]); segments.Add(textSegment); } } diff --git a/src/MarkdownParser/Writer/ViewWriterCache.cs b/src/MarkdownParser/Writer/ViewWriterCache.cs index 7e43367..74c129f 100644 --- a/src/MarkdownParser/Writer/ViewWriterCache.cs +++ b/src/MarkdownParser/Writer/ViewWriterCache.cs @@ -3,6 +3,7 @@ using CommonMark.Syntax; using MarkdownParser.Models; using MarkdownParser.Models.Segments; +using MarkdownParser.Models.Segments.Indicators; namespace MarkdownParser.Writer { @@ -14,7 +15,7 @@ public class ViewWriterCache private readonly Stack<(BaseSegment Segment, T Value)> _valuesStack = new Stack<(BaseSegment, T)>(); // SegmentIndicators that are not ended/closed yet - private readonly List<(Models.Segments.SegmentIndicator SegmentIndicator, int lastCharacterPosition)> _pendingSegmentIndicators = new List<(SegmentIndicator SegmentIndicator, int lastCharacterPosition)>(); + private readonly List<(SegmentIndicator SegmentIndicator, int lastCharacterPosition)> _pendingSegmentIndicators = new List<(SegmentIndicator SegmentIndicator, int lastCharacterPosition)>(); private readonly T _defaultT = default; @@ -37,7 +38,7 @@ public void Add(string item, int firstCharacterPosition) return; } - var segment = new Segment(item); + var segment = new TextSegment(item); _valuesStack.Push((segment, _defaultT)); } @@ -124,7 +125,17 @@ private void FinalizeSegmentIndicator(int toWritePosition) { _pendingSegmentIndicators.Remove(pendingIndicator); - var segmentIndicator = new IndicatorSegment(pendingIndicator.SegmentIndicator, SegmentIndicatorPosition.End); + IndicatorSegment segmentIndicator; + switch (pendingIndicator.SegmentIndicator) + { + case SegmentIndicator.Link: + segmentIndicator = new LinkSegment(pendingIndicator.SegmentIndicator, SegmentIndicatorPosition.End, string.Empty, string.Empty); + break; + default: + segmentIndicator = new IndicatorSegment(pendingIndicator.SegmentIndicator, SegmentIndicatorPosition.End); + break; + } + _valuesStack.Push((segmentIndicator, _defaultT)); } } diff --git a/test/MarkdownParser.Test/MarkdownParser.Test.csproj b/test/MarkdownParser.Test/MarkdownParser.Test.csproj index 1391f1b..066436d 100644 --- a/test/MarkdownParser.Test/MarkdownParser.Test.csproj +++ b/test/MarkdownParser.Test/MarkdownParser.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable diff --git a/test/MarkdownParser.Test/MarkdownParserBlockSegmentsSpecs.cs b/test/MarkdownParser.Test/MarkdownParserBlockSegmentsSpecs.cs new file mode 100644 index 0000000..626eaf5 --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParserBlockSegmentsSpecs.cs @@ -0,0 +1,139 @@ +using FluentAssertions; +using MarkdownParser.Models; +using MarkdownParser.Models.Segments; +using MarkdownParser.Models.Segments.Indicators; +using MarkdownParser.Test.Mocks; +using MarkdownParser.Test.Services; + +namespace MarkdownParser.Test; + +[TestClass] +public class MarkdownParserBlockSegmentsSpecs +{ + [TestMethod] + public void When_parsing_text_with_emphasis_it_should_output_ordered_segments() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("TextEmphasis.basic-text.md"); + + var componentSupplier = new PassThroughComponentSupplier(); + var parser = new MarkdownParser(componentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + + parseResult[0].Should().BeOfType(); + var textBlock0 = parseResult[0] as TextBlock; + textBlock0!.TextSegments.Length.Should().Be(5); + textBlock0!.TextSegments[0].As().Text.Should().Be("Use "); + textBlock0!.TextSegments[1].As().Indicator.Should().Be(SegmentIndicator.Strikethrough); + textBlock0!.TextSegments[1].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock0!.TextSegments[2].As().Text.Should().Be("double"); + textBlock0!.TextSegments[3].As().Indicator.Should().Be(SegmentIndicator.Strikethrough); + textBlock0!.TextSegments[3].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock0!.TextSegments[4].As().Text.Should().Be(" tildes around the words."); + + parseResult[1].Should().BeOfType(); + var textBlock1 = parseResult[1] as TextBlock; + textBlock1!.TextSegments.Length.Should().Be(9); + textBlock1!.TextSegments[0].As().Text.Should().Be("Paragraphs "); + textBlock1!.TextSegments[1].As().Indicator.Should().Be(SegmentIndicator.Strong); + textBlock1!.TextSegments[1].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock1!.TextSegments[2].As().Text.Should().Be("are "); + textBlock1!.TextSegments[3].As().Indicator.Should().Be(SegmentIndicator.Italic); + textBlock1!.TextSegments[3].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock1!.TextSegments[4].As().Text.Should().Be("separated"); + textBlock1!.TextSegments[5].As().Indicator.Should().Be(SegmentIndicator.Italic); + textBlock1!.TextSegments[5].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock1!.TextSegments[6].As().Text.Should().Be(" by"); + textBlock1!.TextSegments[7].As().Indicator.Should().Be(SegmentIndicator.Strong); + textBlock1!.TextSegments[7].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock1!.TextSegments[8].As().Text.Should().Be(" a blank line."); + + parseResult[2].Should().BeOfType(); + var textBlock2 = parseResult[2] as TextBlock; + textBlock2!.TextSegments.Length.Should().Be(15); + textBlock2!.TextSegments[0].As().Text.Should().Be("3rd paragraph. "); + textBlock2!.TextSegments[1].As().Indicator.Should().Be(SegmentIndicator.Italic); + textBlock2!.TextSegments[1].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock2!.TextSegments[2].As().Text.Should().Be("this is Italic"); + textBlock2!.TextSegments[3].As().Indicator.Should().Be(SegmentIndicator.Italic); + textBlock2!.TextSegments[3].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock2!.TextSegments[4].As().Text.Should().Be(", "); + textBlock2!.TextSegments[5].As().Indicator.Should().Be(SegmentIndicator.Strong); + textBlock2!.TextSegments[5].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock2!.TextSegments[6].As().Text.Should().Be("this is bold"); + textBlock2!.TextSegments[7].As().Indicator.Should().Be(SegmentIndicator.Strong); + textBlock2!.TextSegments[7].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock2!.TextSegments[8].As().Text.Should().Be(", and "); + textBlock2!.TextSegments[9].As().Indicator.Should().Be(SegmentIndicator.Code); + textBlock2!.TextSegments[9].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock2!.TextSegments[10].As().Text.Should().Be("monospace"); + textBlock2!.TextSegments[11].As().Indicator.Should().Be(SegmentIndicator.Code); + textBlock2!.TextSegments[11].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock2!.TextSegments[12].As().Text.Should().Be(". Itemized lists"); + textBlock2!.TextSegments[13].As().Indicator.Should().Be(SegmentIndicator.LineBreak); + textBlock2!.TextSegments[14].As().Text.Should().Be("look like:"); + } + + [TestMethod] + public void When_parsing_text_with_links_it_should_output_ordered_segments() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("TextEmphasis.basic-links.md"); + + var componentSupplier = new PassThroughComponentSupplier(); + var parser = new MarkdownParser(componentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(2); + + parseResult[0].Should().BeOfType(); + var textBlock0 = parseResult[0] as TextBlock; + textBlock0!.TextSegments.Length.Should().Be(5); + textBlock0!.TextSegments[0].As().Text.Should().Be("My favorite search engine is "); + textBlock0!.TextSegments[1].As().Indicator.Should().Be(SegmentIndicator.Link); + textBlock0!.TextSegments[1].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock0!.TextSegments[1].As().Title.Should().Be("The best search engine for privacy"); + textBlock0!.TextSegments[1].As().Url.Should().Be("https://duckduckgo.com"); + textBlock0!.TextSegments[2].As().Text.Should().Be("Duck Duck Go"); + textBlock0!.TextSegments[3].As().Indicator.Should().Be(SegmentIndicator.Link); + textBlock0!.TextSegments[3].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock0!.TextSegments[4].As().Text.Should().Be("."); + + parseResult[1].Should().BeOfType(); + var textBlock1 = parseResult[1] as TextBlock; + textBlock1!.TextSegments.Length.Should().Be(7); + textBlock1!.TextSegments[0].As().Indicator.Should().Be(SegmentIndicator.Link); + textBlock1!.TextSegments[0].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock1!.TextSegments[0].As().Url.Should().Be("https://www.markdownguide.org"); + textBlock1!.TextSegments[1].As().Text.Should().Be("https://www.markdownguide.org"); + textBlock1!.TextSegments[2].As().Indicator.Should().Be(SegmentIndicator.Link); + textBlock1!.TextSegments[2].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + textBlock1!.TextSegments[3].As().Indicator.Should().Be(SegmentIndicator.LineBreak); + textBlock1!.TextSegments[4].As().Indicator.Should().Be(SegmentIndicator.Link); + textBlock1!.TextSegments[4].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.Start); + textBlock1!.TextSegments[4].As().Url.Should().Be("mailto:fake@example.com"); + textBlock1!.TextSegments[5].As().Text.Should().Be("fake@example.com"); + textBlock1!.TextSegments[6].As().Indicator.Should().Be(SegmentIndicator.Link); + textBlock1!.TextSegments[6].As().IndicatorPosition.Should().Be(SegmentIndicatorPosition.End); + } +} \ No newline at end of file diff --git a/test/MarkdownParser.Test/MarkdownParserTextSegmentsSpecs.cs b/test/MarkdownParser.Test/MarkdownParserTextSegmentsSpecs.cs index 5486ebe..6beb6f8 100644 --- a/test/MarkdownParser.Test/MarkdownParserTextSegmentsSpecs.cs +++ b/test/MarkdownParser.Test/MarkdownParserTextSegmentsSpecs.cs @@ -1,4 +1,5 @@ -using MarkdownParser.Test.Mocks; +using FluentAssertions; +using MarkdownParser.Test.Mocks; using MarkdownParser.Test.Services; namespace MarkdownParser.Test; @@ -6,9 +7,8 @@ namespace MarkdownParser.Test; [TestClass] public class MarkdownParserTextSegmentsSpecs { - // TODO: test (Bold/Italic/Strikethrough/Links/etc) [TestMethod] - public void When_parsing_paragraphs_it_should_output_multiple_text_views_in_good_order() + public void When_parsing_text_with_emphasis_it_should_output_literal_text_correct() { //----------------------------------------------------------------------------------------------------------- // Arrange @@ -26,9 +26,33 @@ public void When_parsing_paragraphs_it_should_output_multiple_text_views_in_good //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - //parseResult.Count.Should().Be(3); - //parseResult[0].Should().StartWith("textview:Paragraphs are"); - //parseResult[1].Should().StartWith("textview:2nd paragraph."); - //parseResult[2].Should().StartWith("textview:Note that"); + parseResult.Count.Should().Be(3); + parseResult[0].Should().StartWith("textview:Use double tildes around the words."); + parseResult[1].Should().StartWith("textview:Paragraphs are separated by a blank line."); + parseResult[2].Should().StartWith("textview:3rd paragraph. this is Italic, this is bold, and monospace. Itemized lists\r\nlook like:"); + } + + [TestMethod] + public void When_parsing_text_with_links_it_should_output_literal_text_correct() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("TextEmphasis.basic-links.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("textview:My favorite search engine is Duck Duck Go."); + parseResult[1].Should().StartWith("textview:https://www.markdownguide.org\r\nfake@example.com"); } } \ No newline at end of file diff --git a/test/MarkdownParser.Test/Mocks/PassThroughComponentSupplier.cs b/test/MarkdownParser.Test/Mocks/PassThroughComponentSupplier.cs new file mode 100644 index 0000000..1f1a2ee --- /dev/null +++ b/test/MarkdownParser.Test/Mocks/PassThroughComponentSupplier.cs @@ -0,0 +1,73 @@ +using MarkdownParser.Models; + +namespace MarkdownParser.Test.Mocks; + +internal class PassThroughComponentSupplier : IViewSupplier +{ + public MarkdownReferenceDefinition[]? MarkdownReferenceDefinitions { get; private set; } + + public void RegisterReferenceDefinitions(IEnumerable markdownReferenceDefinitions) + { + MarkdownReferenceDefinitions = markdownReferenceDefinitions.ToArray(); + } + + public object GetTextView(TextBlock textBlock) + { + return textBlock; + } + + public object GetBlockquotesView(object childView) + { + return childView; + } + + public object GetHeaderView(TextBlock textBlock, int headerLevel) + { + return textBlock; + } + + public object GetImageView(string url, string subscription, string imageId) + { + return url; + } + + public object GetListView(List items) + { + return items; + } + + public object GetListItemView(object childView, bool isOrderedList, int sequenceNumber, int listLevel) + { + return childView; + } + + public object GetStackLayoutView(List childViews) + { + return childViews; + } + + public object GetThematicBreak() + { + return "ThematicBreak"; + } + + public object GetPlaceholder(string placeholderName) + { + return placeholderName; + } + + public object GetFencedCodeBlock(TextBlock textBlock, string codeInfo) + { + return textBlock; + } + + public object GetIndentedCodeBlock(TextBlock textBlock) + { + return textBlock; + } + + public object GetHtmlBlock(TextBlock textBlock) + { + return textBlock; + } +} \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md b/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md index 3d559e1..f64a494 100644 --- a/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md +++ b/test/MarkdownParser.Test/Resources/Examples/TextEmphasis/basic-text.md @@ -2,5 +2,5 @@ Use ~~double~~ tildes around the words. Paragraphs **are *separated* by** a blank line. -2nd paragraph. *this is Italic*, **this is bold**, and `monospace`. Itemized lists +3rd paragraph. *this is Italic*, **this is bold**, and `monospace`. Itemized lists look like: From be5ea122b8f402853475ae4f308da52135e2fa5c Mon Sep 17 00:00:00 2001 From: Toine db Date: Wed, 13 Nov 2024 20:50:27 +0100 Subject: [PATCH 3/4] net9 pipeline --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 233114d..6955284 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,5 +15,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + - name: Install .NET MAUI + run: dotnet workload install maui - name: Build - run: dotnet build src\MarkdownParser.sln -c Release + run: dotnet build src\MarkdownParser.sln -c Release \ No newline at end of file From 649f73ab86646460473d010aadb72a883d1563a4 Mon Sep 17 00:00:00 2001 From: Toine db Date: Wed, 13 Nov 2024 20:57:41 +0100 Subject: [PATCH 4/4] .net 9 tests --- .github/workflows/ci-test.yml | 4 ++++ .github/workflows/ci.yml | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 130e461..ddebac3 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -15,6 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' - name: Build run: dotnet build test\MarkdownParser.Test.sln -c Release - name: Run Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6955284..6203637 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,5 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: '9.0.x' - - name: Install .NET MAUI - run: dotnet workload install maui - name: Build run: dotnet build src\MarkdownParser.sln -c Release \ No newline at end of file