diff --git a/src/MarkdownParser/Models/BlockType.cs b/src/MarkdownParser/Models/BlockType.cs new file mode 100644 index 0000000..9ca792e --- /dev/null +++ b/src/MarkdownParser/Models/BlockType.cs @@ -0,0 +1,14 @@ +namespace MarkdownParser.Models +{ + public enum BlockType + { + RootOrUnknown, + List, + FencedCode, + IndentedCode, + Html, + Quote, + Heading, + Paragraph + } +} \ No newline at end of file diff --git a/src/MarkdownParser/Models/TextBlock.cs b/src/MarkdownParser/Models/TextBlock.cs index a94e8da..6ae6243 100644 --- a/src/MarkdownParser/Models/TextBlock.cs +++ b/src/MarkdownParser/Models/TextBlock.cs @@ -6,6 +6,11 @@ public class TextBlock { public BaseSegment[] TextSegments { get; } + /// + /// Ancestors in order of (starting at 0) GrandParent > Parent > Sibling + /// + public BlockType[] AncestorsTree { get; internal set; } + public TextBlock(BaseSegment[] textSegments) { TextSegments = textSegments; diff --git a/src/MarkdownParser/Writer/ViewWriter.cs b/src/MarkdownParser/Writer/ViewWriter.cs index 3161e10..bab9dc9 100644 --- a/src/MarkdownParser/Writer/ViewWriter.cs +++ b/src/MarkdownParser/Writer/ViewWriter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -72,7 +73,7 @@ public void StartBlock(BlockTag blockType) { Workbench.Push(new ViewWriterCache { ComponentType = blockType }); } - + public void FinalizeParagraphBlock() { var wbi = GetWorkbenchItem(); @@ -89,9 +90,16 @@ public void FinalizeParagraphBlock() foreach (var itemsCacheTuple in itemsCache) { - var view = itemsCacheTuple.TextBlock != null - ? ViewSupplier.CreateTextView(itemsCacheTuple.TextBlock) - : itemsCacheTuple.Value; + T view; + if (itemsCacheTuple.TextBlock != null) + { + itemsCacheTuple.TextBlock.AncestorsTree = GetAncestorsTreeFromWorkbench(BlockType.Paragraph); + view = ViewSupplier.CreateTextView(itemsCacheTuple.TextBlock); + } + else + { + view = itemsCacheTuple.Value; + } if (view != null) { @@ -139,9 +147,16 @@ public void FinalizeHeaderBlock(int headerLevel) foreach (var itemsCacheTuple in itemsCache) { - var view = itemsCacheTuple.TextBlock != null - ? ViewSupplier.CreateHeaderView(itemsCacheTuple.TextBlock, headerLevel) - : itemsCacheTuple.Value; + T view; + if (itemsCacheTuple.TextBlock != null) + { + itemsCacheTuple.TextBlock.AncestorsTree = GetAncestorsTreeFromWorkbench(BlockType.Heading); + view = ViewSupplier.CreateHeaderView(itemsCacheTuple.TextBlock, headerLevel); + } + else + { + view = itemsCacheTuple.Value; + } views.Add(view); } @@ -187,9 +202,16 @@ public void FinalizeListItemBlock(ListData listData) foreach (var itemsCacheTuple in itemsCache) { - var view = itemsCacheTuple.TextBlock != null - ? ViewSupplier.CreateTextView(itemsCacheTuple.TextBlock) - : itemsCacheTuple.Value; + T view; + if (itemsCacheTuple.TextBlock != null) + { + itemsCacheTuple.TextBlock.AncestorsTree = GetAncestorsTreeFromWorkbench(BlockType.List); + view = ViewSupplier.CreateTextView(itemsCacheTuple.TextBlock); + } + else + { + view = itemsCacheTuple.Value; + } if (view != null) { @@ -257,6 +279,7 @@ public void StartAndFinalizeImageBlock(string targetUrl, string subscription, st public void StartAndFinalizeFencedCodeBlock(StringContent content, string blockInfo) { var blocks = StringContentToBlocks(content); + blocks.AncestorsTree = GetAncestorsTreeFromWorkbench(BlockType.FencedCode); var blockView = ViewSupplier.CreateFencedCodeBlock(blocks, blockInfo); StoreView(blockView); @@ -265,6 +288,7 @@ public void StartAndFinalizeFencedCodeBlock(StringContent content, string blockI public void StartAndFinalizeIndentedCodeBlock(StringContent content) { var blocks = StringContentToBlocks(content); + blocks.AncestorsTree = GetAncestorsTreeFromWorkbench(BlockType.IndentedCode); var blockView = ViewSupplier.CreateIndentedCodeBlock(blocks); StoreView(blockView); @@ -273,6 +297,7 @@ public void StartAndFinalizeIndentedCodeBlock(StringContent content) public void StartAndFinalizeHtmlBlock(StringContent content) { var blocks = StringContentToBlocks(content); + blocks.AncestorsTree = GetAncestorsTreeFromWorkbench(BlockType.Html); var blockView = ViewSupplier.CreateHtmlBlock(blocks); StoreView(blockView); @@ -290,6 +315,56 @@ public void StartAndFinalizePlaceholderBlock(string placeholderName) StoreView(placeholderView); } + private BlockType[] GetAncestorsTreeFromWorkbench(BlockType currentBlockType) + { + var blockTypeTree = new List(); + foreach (var workBenchItem in Workbench.Reverse()) + { + switch (workBenchItem.ComponentType) + { + case BlockTag.BlockQuote: + blockTypeTree.Add(BlockType.Quote); + break; + case BlockTag.List: + blockTypeTree.Add(BlockType.List); + break; + case BlockTag.FencedCode: + blockTypeTree.Add(BlockType.FencedCode); + break; + case BlockTag.IndentedCode: + blockTypeTree.Add(BlockType.IndentedCode); + break; + case BlockTag.HtmlBlock: + blockTypeTree.Add(BlockType.Html); + break; + case BlockTag.Paragraph: + blockTypeTree.Add(BlockType.Paragraph); + break; + case BlockTag.AtxHeading: + case BlockTag.SetextHeading: + blockTypeTree.Add(BlockType.Heading); + break; + case BlockTag.ListItem: // this is already covered by BlockTag.List + case BlockTag.Document: + case BlockTag.ThematicBreak: + case BlockTag.ReferenceDefinition: + case null: + default: + break; + } + } + + // Add current BlockType as most recent Ancestor + // but skip Lists as current to prevent double List entries + // because current List and ListItem levels are already picked up from Workbench + if (currentBlockType != BlockType.List) + { + blockTypeTree.Add(currentBlockType); + } + + return blockTypeTree.ToArray(); + } + private T StackViews(List views) { if (views == null diff --git a/test/MarkdownParser.Test/MarkdownParser.Test.csproj b/test/MarkdownParser.Test/MarkdownParser.Test.csproj index 066436d..7f4eb56 100644 --- a/test/MarkdownParser.Test/MarkdownParser.Test.csproj +++ b/test/MarkdownParser.Test/MarkdownParser.Test.csproj @@ -28,6 +28,10 @@ + + + + diff --git a/test/MarkdownParser.Test/MarkdownParserBlocksSpecs.cs b/test/MarkdownParser.Test/MarkdownParserBlocksSpecs.cs new file mode 100644 index 0000000..5188952 --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParserBlocksSpecs.cs @@ -0,0 +1,294 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using MarkdownParser.Models; +using MarkdownParser.Test.Mocks; +using MarkdownParser.Test.Services; +using static System.Net.Mime.MediaTypeNames; + +namespace MarkdownParser.Test; + +[TestClass] +public class MarkdownParserBlocksSpecs +{ + [TestMethod] + public void When_parsing_paragraphs_it_should_output_paragraph_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.paragraphs.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + parseResult[0].Split(':')[1].Should().Be($"[]{BlockType.Paragraph}"); + parseResult[1].Split(':')[1].Should().Be($"[]{BlockType.Paragraph}"); + parseResult[2].Split(':')[1].Should().Be($"[]{BlockType.Paragraph}"); + } + + [TestMethod] + public void When_parsing_headers_it_should_output_header_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.headers.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + parseResult[0].Split(':')[2].Should().Be($"[]{BlockType.Heading}"); + parseResult[1].Split(':')[2].Should().Be($"[]{BlockType.Heading}"); + parseResult[2].Split(':')[2].Should().Be($"[]{BlockType.Heading}"); + } + + [TestMethod] + public void When_parsing_nested_list_it_should_output_nesting_list_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.nestedlist.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); // just becuase it only outputs a single string + + var listviewCount = Regex.Matches(parseResult[0], "listview>:").Cast().Count(); + listviewCount.Should().Be(2); + + var splittedViews = parseResult[0].Split(':'); + splittedViews[0].Should().Be("listview>"); + splittedViews[1].Should().Be("-listitemview"); + splittedViews[2].Should().Be("_False.1.1_textview"); + splittedViews[3].Should().Be($"[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[4].Should().Be("item1-listitemview"); + splittedViews[5].Should().Be("_False.1.1_stackview>"); + splittedViews[6].Should().Be("+textview"); + splittedViews[7].Should().Be($"[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[8].Should().Be("item2+listview>"); + splittedViews[9].Should().Be("-listitemview"); + splittedViews[10].Should().Be("_False.2.1_textview"); + splittedViews[11].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[12].Should().Be("item2-1-listitemview"); + splittedViews[13].Should().Be("_False.2.1_textview"); + splittedViews[14].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[15].Should().Be("item2-2-listitemview"); + splittedViews[16].Should().Be("_False.2.1_textview"); + splittedViews[17].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[18].Should().Be("item2-3(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + var splittedViews = parseResult[0].Split(':'); + splittedViews[4].Should().Be($"[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[9].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Heading}"); + splittedViews[12].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[16].Should().Be($"[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[22].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Quote}[]{BlockType.Paragraph}"); + splittedViews[25].Should().Be($"[]{BlockType.List}[]{BlockType.List}[]{BlockType.Quote}[]{BlockType.Heading}"); + } + + [TestMethod] + public void When_parsing_codeblocks_it_should_output_code_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.codeblocks.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(2); + + var codeViewComponentsGroup0 = parseResult[0].Split('|'); + codeViewComponentsGroup0[0].Should().Be("fencedcodeview>"); + codeViewComponentsGroup0[3].Should().Be($"[]{BlockType.FencedCode}"); + + var codeViewComponentsGroup1 = parseResult[1].Split('|'); + codeViewComponentsGroup1[0].Should().Be("indentedview>"); + codeViewComponentsGroup1[2].Should().Be($"[]{BlockType.IndentedCode}"); + } + + [TestMethod] + public void When_parsing_codeblocks_it_should_output_code_ancestors_and_ignores_inner_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.innercodeblocks.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult[0].Should().Contain("[]", Exactly.Once()); + parseResult[0].Should().Contain($"[]{BlockType.FencedCode}"); + parseResult[1].Should().Contain("[]", Exactly.Once()); + parseResult[1].Should().Contain($"[]{BlockType.IndentedCode}"); + } + + [TestMethod] + public void When_parsing_htmlblocks_it_should_output_html_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.htmlblocks.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + var htmlViewComponentsGroup0 = parseResult[0].Split('|'); + htmlViewComponentsGroup0.First().Should().Be("htmlview>"); + htmlViewComponentsGroup0[2].Should().Be($"[]{BlockType.Html}"); + + var htmlViewComponentsGroup1 = parseResult[1].Split('|'); + htmlViewComponentsGroup1.First().Should().Be("htmlview>"); + htmlViewComponentsGroup1[2].Should().Be($"[]{BlockType.Html}"); + } + + [TestMethod] + public void When_parsing_htmlblocks_it_should_output_html_ancestors_and_ignores_inner_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.innerhtmlblocks.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult[0].Should().Contain("[]", Exactly.Once()); + parseResult[0].Should().Contain($"[]{BlockType.Html}"); + } + + [TestMethod] + public void When_parsing_reference_definitions_it_should_output_correct_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.referencedefinitions.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult[0].Should().StartWith($"stackview>:+textview:[]{BlockType.Paragraph}"); + } + + [TestMethod] + public void When_parsing_quotes_it_should_output_quote_ancestors_with_inner_ancestors() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.innerblockquotes.md"); + + var mockComponentSupplier = new BlockComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + var splittedViews = parseResult[0].Split(':'); + splittedViews[3].Should().Be($"[]{BlockType.Quote}[]{BlockType.Paragraph}"); + splittedViews[5].Should().Be($"[]{BlockType.Quote}[]{BlockType.Paragraph}"); + splittedViews[8].Should().Be($"[]{BlockType.Quote}[]{BlockType.Heading}"); + splittedViews[10].Should().Be($"[]{BlockType.Quote}[]{BlockType.Paragraph}"); + splittedViews[14].Should().Be($"[]{BlockType.Quote}[]{BlockType.List}[]{BlockType.Paragraph}"); + splittedViews[17].Should().Be($"[]{BlockType.Quote}[]{BlockType.List}[]{BlockType.Paragraph}"); + } + +} diff --git a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs index 492fac7..c9449ca 100644 --- a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs +++ b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs @@ -53,9 +53,9 @@ public void When_parsing_headers_it_should_output_header_views() // Assert //----------------------------------------------------------------------------------------------------------- parseResult.Count.Should().Be(3); - parseResult[0].Should().StartWith("headerview:"); - parseResult[1].Should().StartWith("headerview:"); - parseResult[1].Should().StartWith("headerview:"); + parseResult[0].Should().StartWith("headerview:1:"); + parseResult[1].Should().StartWith("headerview:2:"); + parseResult[2].Should().StartWith("headerview:3:"); } [TestMethod] diff --git a/test/MarkdownParser.Test/Mocks/BlockComponentSupplier.cs b/test/MarkdownParser.Test/Mocks/BlockComponentSupplier.cs new file mode 100644 index 0000000..8a0c14e --- /dev/null +++ b/test/MarkdownParser.Test/Mocks/BlockComponentSupplier.cs @@ -0,0 +1,110 @@ +using MarkdownParser.Models; +using Microsoft.VisualBasic; + +namespace MarkdownParser.Test.Mocks; + +internal class BlockComponentSupplier : IViewSupplier +{ + public MarkdownReferenceDefinition[]? MarkdownReferenceDefinitions { get; private set; } + + public void OnReferenceDefinitionsPublished(IEnumerable markdownReferenceDefinitions) + { + MarkdownReferenceDefinitions = markdownReferenceDefinitions.ToArray(); + } + + private string AncestorsTreeToString(BlockType[] textBlockParentBlockTypeTree) + { + return textBlockParentBlockTypeTree.Aggregate(string.Empty, (current, blockType) => current + $"[]{blockType}"); + } + + public string CreateTextView(TextBlock textBlock) + { + var content = textBlock.ExtractLiteralContent(Settings.TextualLineBreak); + return $"textview:{AncestorsTreeToString(textBlock.AncestorsTree)}:{content}"; + } + + public string CreateBlockquotesView(string content) + { + return $"blockquoteview>:{content} items) + { + // Each item will start with a '-' + var listItems = items.Aggregate(string.Empty, (current, item) => current + $"-{item}"); + + return $"listview>:{listItems} childViews) + { + var listItems = childViews.Aggregate(string.Empty, (current, item) => current + $"+{item}"); + + return $"stackview>:{listItems}|({codeInfo})|{content}|{AncestorsTreeToString(textBlock.AncestorsTree)}||{content}|{AncestorsTreeToString(textBlock.AncestorsTree)}||{content}|{AncestorsTreeToString(textBlock.AncestorsTree)}| Block quotes are +> written like so. +> +> They can span multiple paragraphs, +> +> # Sometimes contain headers +> +> or even lists +> * item1 +> * item2 \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/innercodeblocks.md b/test/MarkdownParser.Test/Resources/Examples/Sections/innercodeblocks.md new file mode 100644 index 0000000..197701b --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/innercodeblocks.md @@ -0,0 +1,9 @@ +```cs +var myNumber = 1; +* list item1 +* list item2 +``` + + the first line for IndentedCode code block + # second line as header + \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/innerhtmlblocks.md b/test/MarkdownParser.Test/Resources/Examples/Sections/innerhtmlblocks.md new file mode 100644 index 0000000..7cf0e9b --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/innerhtmlblocks.md @@ -0,0 +1,11 @@ +

First text in block

+
+

Header

+ # Markdown header notation +

+ Paragraph with list + * item1 + * item2 +

+
+ diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/innernestedlist.md b/test/MarkdownParser.Test/Resources/Examples/Sections/innernestedlist.md new file mode 100644 index 0000000..32ddac0 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/innernestedlist.md @@ -0,0 +1,7 @@ +* item1 + * # item1.1 with Header + * item1.2 Some test + with multiple lines +* item2 + * > inner Quote + > # Quote With header \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md b/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md index dec6d66..9059e16 100644 --- a/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md @@ -1,7 +1,7 @@ Paragraphs are separated by a blank line. 2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists -look like: (removed) +look like (removed)