From 2b07e9a5b9fb40ad3e6b98dcc4d5980ab2a737ef Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 8 Aug 2020 14:41:02 +0200 Subject: [PATCH 001/120] add naive cst implementation --- src/Markdig.Tests/TestClassAttribute.cs | 8 ++ src/Markdig.Tests/TestFencedCodeBlock.cs | 67 +++++++++++ src/Markdig.Tests/TestInlineLinkParser.cs | 32 +++++ src/Markdig.Tests/TestParagraphParser.cs | 32 +++++ src/Markdig.Tests/TestQuoteBlock.cs | 32 +++++ .../CustomContainers/CustomContainer.cs | 8 +- src/Markdig/Parsers/FencedBlockParserBase.cs | 111 +++++++++++++++++- src/Markdig/Parsers/ParagraphBlockParser.cs | 4 +- .../Renderers/Normalize/CodeBlockRenderer.cs | 16 ++- .../Normalize/Inlines/LinkInlineRenderer.cs | 8 ++ src/Markdig/Syntax/ContainerBlock.cs | 2 + src/Markdig/Syntax/FencedCodeBlock.cs | 14 ++- src/Markdig/Syntax/IFencedBlock.cs | 20 +++- 13 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 src/Markdig.Tests/TestClassAttribute.cs create mode 100644 src/Markdig.Tests/TestFencedCodeBlock.cs create mode 100644 src/Markdig.Tests/TestInlineLinkParser.cs create mode 100644 src/Markdig.Tests/TestParagraphParser.cs create mode 100644 src/Markdig.Tests/TestQuoteBlock.cs diff --git a/src/Markdig.Tests/TestClassAttribute.cs b/src/Markdig.Tests/TestClassAttribute.cs new file mode 100644 index 000000000..f0d30790e --- /dev/null +++ b/src/Markdig.Tests/TestClassAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Markdig.Tests +{ + internal class TestClassAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Markdig.Tests/TestFencedCodeBlock.cs b/src/Markdig.Tests/TestFencedCodeBlock.cs new file mode 100644 index 000000000..fa7c1fcf7 --- /dev/null +++ b/src/Markdig.Tests/TestFencedCodeBlock.cs @@ -0,0 +1,67 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using NUnit.Framework; +using System.IO; + +namespace Markdig.Tests +{ + [TestFixture] + public partial class TestFencedCodeBlock + { + [Test] + public void CstInfoParser_ParsesHappyFlow() + { + string markdown = $@" +``` derpy args +``` +"; + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var fencedCodeBlock = markdownDocument[0] as FencedCodeBlock; + + Assert.AreEqual('`', fencedCodeBlock.FencedChar); + Assert.AreEqual(3, fencedCodeBlock.FencedCharCount); + Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterFencedChar); + Assert.AreEqual("derpy", fencedCodeBlock.Info); + Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterInfo); + Assert.AreEqual("args", fencedCodeBlock.Arguments); + Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterArguments); + } + + [Test] + public void CstInfoParser_ParsesComplicatedArguments() + { + string markdown = "``` derpy args more\t args \t\t and even more args \t \r\n```"; + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var fencedCodeBlock = markdownDocument[0] as FencedCodeBlock; + + Assert.AreEqual('`', fencedCodeBlock.FencedChar); + Assert.AreEqual(3, fencedCodeBlock.FencedCharCount); + Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterFencedChar); + Assert.AreEqual("derpy", fencedCodeBlock.Info); + Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterInfo); + Assert.AreEqual("args more\t args \t\t and even more args", fencedCodeBlock.Arguments); + Assert.AreEqual(" \t ", fencedCodeBlock.WhitespaceAfterArguments); + } + + [Test] + public void CstInfoParser_RoundTrip() + { + string markdown = "``` derpy args more\t args \t\t and even more args \t \n```"; + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + } +} diff --git a/src/Markdig.Tests/TestInlineLinkParser.cs b/src/Markdig.Tests/TestInlineLinkParser.cs new file mode 100644 index 000000000..e86cea77a --- /dev/null +++ b/src/Markdig.Tests/TestInlineLinkParser.cs @@ -0,0 +1,32 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using NUnit.Framework; +using System.IO; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestInlineLinkParser + { + [Test] + public void Test() + { + string markdown = " ![ description ]( http://example.com )"; + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as ParagraphBlock; + var containerInline = paragraphBlock.Inline as ContainerInline; + var linkInline = containerInline.FirstChild as LinkInline; + var description = linkInline.FirstChild as LiteralInline; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + } +} diff --git a/src/Markdig.Tests/TestParagraphParser.cs b/src/Markdig.Tests/TestParagraphParser.cs new file mode 100644 index 000000000..9279b8f8b --- /dev/null +++ b/src/Markdig.Tests/TestParagraphParser.cs @@ -0,0 +1,32 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestParagraphParser + { + [Test] + public void Test() + { + var markdown = " This is a paragraph. "; + + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as ParagraphBlock; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + } +} diff --git a/src/Markdig.Tests/TestQuoteBlock.cs b/src/Markdig.Tests/TestQuoteBlock.cs new file mode 100644 index 000000000..7654a6a36 --- /dev/null +++ b/src/Markdig.Tests/TestQuoteBlock.cs @@ -0,0 +1,32 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestQuoteBlock + { + [Test] + public void Test() + { + var markdown = " > This is a quote with whitespace before, between and after. "; + + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as QuoteBlock; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + } +} diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index 193ce5957..dab05c618 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -22,12 +22,18 @@ public CustomContainer(BlockParser parser) : base(parser) { } + public char FencedChar { get; set; } + + public string WhitespaceAfterFencedChar { get; set; } + public string Info { get; set; } + public string WhitespaceAfterInfo { get; set; } + public string Arguments { get; set; } + public string WhitespaceAfterArguments { get; set; } public int FencedCharCount { get; set; } - public char FencedChar { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index e510ce8da..8463d26d3 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -45,7 +45,7 @@ public abstract class FencedBlockParserBase : FencedBlockParserBase where T : /// protected FencedBlockParserBase() { - InfoParser = DefaultInfoParser; + InfoParser = CstInfoParser; MinimumMatchCount = 3; MaximumMatchCount = int.MaxValue; } @@ -59,6 +59,113 @@ protected FencedBlockParserBase() public int MaximumMatchCount { get; set; } + private enum ParseState + { + AfterFence, + Info, + AfterInfo, + Args, + AfterArgs, + } + + /// + /// The CST parser for the information after the fenced code block special characters (usually ` or ~) + /// + /// The parser processor. + /// The line. + /// The fenced code block. + /// The opening character for this fenced code block. + /// true if parsing of the line is successfull; false otherwise + public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + { + string afterFence = null; + string info = null; + string afterInfo = null; + string arg = null; + string afterArg = null; + ParseState state = ParseState.AfterFence; + + // pattern: ``` info? args? + // after blockchar? + // after info? + // after args? + // info: between fencedchars and + + // An info string cannot contain any backticks (unless it is a tilde block) + for (int i = line.Start; i <= line.End; i++) + { + char c = line.Text[i]; + switch (state) + { + case ParseState.AfterFence: + if (c.IsSpaceOrTab()) + { + afterFence += c; + } + else + { + state = ParseState.Info; + info += c; + } + break; + case ParseState.Info: + if (c.IsSpaceOrTab()) + { + state = ParseState.AfterInfo; + afterInfo += c; + } + else + { + info += c; + } + break; + case ParseState.AfterInfo: + if (c.IsSpaceOrTab()) + { + afterInfo += c; + } + else + { + arg += c; + state = ParseState.Args; + } + break; + case ParseState.Args: + var start = i - 1; + // walk from end, as rest (including spaces except trailing spaces) is args + for (int j = line.End; j > start; j--) + { + char cc = line[j]; + if (cc.IsSpaceOrTab()) + { + afterArg = cc + afterArg; + } + else + { + var length = j - start + 1; + arg = line.Text.Substring(start, length); + goto end; + } + } + goto end; + case ParseState.AfterArgs: + { + //throw new Exception("should ot reach this code"); + return false; + } + } + } + + end: + fenced.WhitespaceAfterFencedChar = afterFence; + fenced.Info = HtmlHelper.Unescape(info); + fenced.WhitespaceAfterInfo = afterInfo; + fenced.Arguments = HtmlHelper.Unescape(arg); + fenced.WhitespaceAfterArguments = afterArg; + + return true; + } + /// /// The default parser for the information after the fenced code block special characters (usually ` or ~) /// @@ -156,7 +263,7 @@ public override BlockState TryOpen(BlockProcessor processor) } // specs spaces: Is space and tabs? or only spaces? Use space and tab for this case - line.TrimStart(); + //line.TrimStart(); var fenced = CreateFencedBlock(processor); { diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 0c3ddcdcb..d7bf9b892 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -65,10 +65,10 @@ public override bool Close(BlockProcessor processor, Block block) for (int i = 0; i < lineCount; i++) { - lines.Lines[i].Slice.TrimStart(); + //lines.Lines[i].Slice.TrimStart(); } - lines.Lines[lineCount - 1].Slice.TrimEnd(); + //lines.Lines[lineCount - 1].Slice.TrimEnd(); } return true; diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index c9cb26136..4a6f11e93 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -20,13 +20,27 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) { var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.FencedCharCount); renderer.Write(opening); + + if (fencedCodeBlock.WhitespaceAfterFencedChar != null) + { + renderer.Write(fencedCodeBlock.WhitespaceAfterFencedChar); + } if (fencedCodeBlock.Info != null) { renderer.Write(fencedCodeBlock.Info); } + if (fencedCodeBlock.WhitespaceAfterInfo != null) + { + renderer.Write(fencedCodeBlock.WhitespaceAfterInfo); + } if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) { - renderer.Write(" ").Write(fencedCodeBlock.Arguments); + renderer + .Write(fencedCodeBlock.Arguments); + } + if (fencedCodeBlock.WhitespaceAfterArguments != null) + { + renderer.Write(fencedCodeBlock.WhitespaceAfterArguments); } /* TODO do we need this causes a empty space and would render html attributes to markdown. diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index 1357ce93c..ef0c838c2 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -14,6 +14,14 @@ public class LinkInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, LinkInline link) { + foreach (var child in link) + { + if (child is LiteralInline li) + { + renderer.Write(li.Content.Text); + } + } + return; if (link.IsImage) { renderer.Write('!'); diff --git a/src/Markdig/Syntax/ContainerBlock.cs b/src/Markdig/Syntax/ContainerBlock.cs index d7ffaf24f..73140675e 100644 --- a/src/Markdig/Syntax/ContainerBlock.cs +++ b/src/Markdig/Syntax/ContainerBlock.cs @@ -35,6 +35,8 @@ protected ContainerBlock(BlockParser parser) : base(parser) /// public Block LastChild => Count > 0 ? children[Count - 1] : null; + public string BeforeWhitespace { get; set; } + /// /// Specialize enumerator. /// diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index 086d2a077..1aafeff29 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -27,18 +27,21 @@ public FencedCodeBlock(BlockParser parser) : base(parser) IsBreakable = false; } - /// - /// Gets or sets the language parsed after the first line of - /// the fenced code block. May be null. - /// + /// public string Info { get; set; } + /// + public string WhitespaceAfterInfo { get; set; } + /// /// Gets or sets the arguments after the . /// May be null. /// public string Arguments { get; set; } + /// + public string WhitespaceAfterArguments { get; set; } + /// /// Gets or sets the fenced character count used to open this fenced code block. /// @@ -49,6 +52,9 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// public char FencedChar { get; set; } + /// + public string WhitespaceAfterFencedChar { get; set; } + /// /// Gets or sets the indent count when the fenced code block was indented /// and we need to remove up to indent count chars spaces from the begining of a line. diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 6bd65fd65..d2cd3068d 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// Copyright (c) Alexandre Mutel. All rights reserved. // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. @@ -15,12 +15,30 @@ public interface IFencedBlock : IBlock /// string Info { get; set; } + /// + /// Gets or sets the language parsed after the first line of + /// the fenced code block and includes any whitespace. May be null. + /// + string WhitespaceAfterInfo { get; set; } + /// /// Gets or sets the arguments after the . /// May be null. /// string Arguments { get; set; } + /// + /// Gets or sets the arguments after the . + /// Includes any whitespace. May be null. + /// + string WhitespaceAfterArguments { get; set; } + + /// + /// Gets or sets the arguments after the . + /// Includes any whitespace. May be null. + /// + string WhitespaceAfterFencedChar { get; set; } + /// /// Gets or sets the fenced character count used to open this fenced code block. /// From 147698daab7a55cd0257c65576cb07c4bb45376d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 8 Aug 2020 14:54:57 +0200 Subject: [PATCH 002/120] revert LinkInlineRenderer --- .../Normalize/Inlines/LinkInlineRenderer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index ef0c838c2..9b763b155 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -14,14 +14,14 @@ public class LinkInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, LinkInline link) { - foreach (var child in link) - { - if (child is LiteralInline li) - { - renderer.Write(li.Content.Text); - } - } - return; + //foreach (var child in link) + //{ + // if (child is LiteralInline li) + // { + // renderer.Write(li.Content.Text); + // } + //} + //return; if (link.IsImage) { renderer.Write('!'); From c73785372b29185c06255ac4ad42597d48b4ec3d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 8 Aug 2020 17:22:53 +0200 Subject: [PATCH 003/120] implement cst for Paragraph --- src/Markdig.Tests/TestInlineLinkParser.cs | 20 ++++++++++++++++++ src/Markdig.Tests/TestParagraphParser.cs | 21 ++++++++++++++++++- src/Markdig/Parsers/BlockProcessor.cs | 21 +++++++++++++++++++ src/Markdig/Parsers/MarkdownParser.cs | 2 ++ src/Markdig/Parsers/ParagraphBlockParser.cs | 8 +++++-- .../Renderers/Normalize/ParagraphRenderer.cs | 19 ++++++++++++++++- src/Markdig/Syntax/Block.cs | 7 +++++++ src/Markdig/Syntax/ContainerBlock.cs | 2 -- 8 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/Markdig.Tests/TestInlineLinkParser.cs b/src/Markdig.Tests/TestInlineLinkParser.cs index e86cea77a..98e7abd1b 100644 --- a/src/Markdig.Tests/TestInlineLinkParser.cs +++ b/src/Markdig.Tests/TestInlineLinkParser.cs @@ -28,5 +28,25 @@ public void Test() Assert.AreEqual(markdown, sw.ToString()); } + + [Test] + public void Test1() + { + string markdown = " ![description](http://example.com)"; + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as ParagraphBlock; + var containerInline = paragraphBlock.Inline as ContainerInline; + var linkInline = containerInline.FirstChild as LinkInline; + var description = linkInline.FirstChild as LiteralInline; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } } } diff --git a/src/Markdig.Tests/TestParagraphParser.cs b/src/Markdig.Tests/TestParagraphParser.cs index 9279b8f8b..0db6cf38a 100644 --- a/src/Markdig.Tests/TestParagraphParser.cs +++ b/src/Markdig.Tests/TestParagraphParser.cs @@ -12,7 +12,7 @@ namespace Markdig.Tests public class TestParagraphParser { [Test] - public void Test() + public void TestWhitespaceBefore() { var markdown = " This is a paragraph. "; @@ -28,5 +28,24 @@ public void Test() Assert.AreEqual(markdown, sw.ToString()); } + + [Test] + public void TestNewLinesBeforeAndAfter() + { + var markdown = " \n \nLine2\n\nLine1\n\n"; + //var markdown = "\r\nLine2\r\n\r\nLine1\r\n\r\n"; + + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as ParagraphBlock; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } } } diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index bd838a953..8ae35bf39 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -19,6 +19,7 @@ public class BlockProcessor private int currentStackIndex; private readonly BlockParserStateCache parserStateCache; private int originalLineStart = 0; + private List beforeLines; private BlockProcessor(BlockProcessor root) { @@ -161,6 +162,17 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo /// private bool ContinueProcessingLine { get; set; } + public StringSlice BeforeWhitespace + { + get + { + // TODO: make lazy + return new StringSlice(Line.Text, StartBeforeIndent, ColumnBeforeIndent); + } + } + + public List BeforeLines { get => beforeLines; set => beforeLines = value; } + /// /// Get the current Container that is currently opened /// @@ -695,6 +707,7 @@ private void TryOpenBlocks() if (TryOpenBlocks(globalParsers)) { RestartIndent(); + //RestartBeforeLines(); continue; } } @@ -703,6 +716,11 @@ private void TryOpenBlocks() } } + private void RestartBeforeLines() + { + beforeLines = null; + } + /// /// Tries to open new blocks using the specified list of /// @@ -715,6 +733,9 @@ private bool TryOpenBlocks(BlockParser[] parsers) var blockParser = parsers[j]; if (Line.IsEmpty) { + BeforeLines ??= new List(); + Line.Start = StartBeforeIndent; + BeforeLines.Add(Line); ContinueProcessingLine = false; break; } diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index b414db319..ab5ef9977 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -114,6 +114,8 @@ private void ProcessBlocks() // If this is the end of file and the last line is empty if (lineText.Text is null) { + var lastBlock = blockProcessor.LastBlock; + lastBlock.LinesAfter = blockProcessor.BeforeLines; break; } blockProcessor.ProcessLine(lineText); diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index d7bf9b892..bd0ee0d5f 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -4,6 +4,7 @@ using Markdig.Helpers; using Markdig.Syntax; +using System.Diagnostics; namespace Markdig.Parsers { @@ -21,12 +22,15 @@ public override BlockState TryOpen(BlockProcessor processor) { return BlockState.None; } - + var linesBefore = processor.BeforeLines; + processor.BeforeLines = null; // We continue trying to match by default processor.NewBlocks.Push(new ParagraphBlock(this) { Column = processor.Column, - Span = new SourceSpan(processor.Line.Start, processor.Line.End) + Span = new SourceSpan(processor.Line.Start, processor.Line.End), + BeforeWhitespace = processor.BeforeWhitespace.ToString(), + LinesBefore = linesBefore }); return BlockState.Continue; } diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 6fa0f65af..38ff2e946 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -14,8 +14,25 @@ public class ParagraphRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, ParagraphBlock obj) { + if (obj.LinesBefore != null) + { + foreach (var line in obj.LinesBefore) + { + renderer.WriteLine(line.ToString()); + } + } + + renderer.Write(obj.BeforeWhitespace); renderer.WriteLeafInline(obj); - renderer.FinishBlock(!renderer.CompactParagraph); + renderer.WriteLine(); + + if (obj.LinesAfter != null) + { + foreach (var line in obj.LinesAfter) + { + renderer.WriteLine(line.ToString()); + } + } } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index 1bc22d7a4..de22908b2 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; +using System.Collections.Generic; namespace Markdig.Syntax { @@ -48,6 +50,11 @@ protected Block(BlockParser parser) /// public bool RemoveAfterProcessInlines { get; set; } + public string BeforeWhitespace { get; set; } + + public List LinesBefore { get; set; } + public List LinesAfter { get; internal set; } + /// /// Occurs when the process of inlines begin. /// diff --git a/src/Markdig/Syntax/ContainerBlock.cs b/src/Markdig/Syntax/ContainerBlock.cs index 73140675e..d7ffaf24f 100644 --- a/src/Markdig/Syntax/ContainerBlock.cs +++ b/src/Markdig/Syntax/ContainerBlock.cs @@ -35,8 +35,6 @@ protected ContainerBlock(BlockParser parser) : base(parser) /// public Block LastChild => Count > 0 ? children[Count - 1] : null; - public string BeforeWhitespace { get; set; } - /// /// Specialize enumerator. /// From 30f670bf5fff84a2731968957d4e6e34189adb47 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 8 Aug 2020 17:24:16 +0200 Subject: [PATCH 004/120] fix NRE --- src/Markdig/Parsers/MarkdownParser.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index ab5ef9977..98fa28b43 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -115,7 +115,10 @@ private void ProcessBlocks() if (lineText.Text is null) { var lastBlock = blockProcessor.LastBlock; - lastBlock.LinesAfter = blockProcessor.BeforeLines; + if (lastBlock != null) + { + lastBlock.LinesAfter = blockProcessor.BeforeLines; + } break; } blockProcessor.ProcessLine(lineText); From cd18087e2943daf4114fc1b04d43512ee130276b Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 8 Aug 2020 18:18:55 +0200 Subject: [PATCH 005/120] implement cst for header --- src/Markdig.Tests/TestParagraphParser.cs | 23 +++++++++++++++ src/Markdig/Helpers/LineReader.cs | 5 ++++ src/Markdig/Parsers/BlockProcessor.cs | 7 +++++ src/Markdig/Parsers/HeadingBlockParser.cs | 4 ++- src/Markdig/Parsers/MarkdownParser.cs | 10 +++++-- src/Markdig/Parsers/ParagraphBlockParser.cs | 5 ++-- .../Renderers/Normalize/HeadingRenderer.cs | 7 ++++- .../Renderers/Normalize/NormalizeRenderer.cs | 28 +++++++++++++++---- .../Renderers/Normalize/ParagraphRenderer.cs | 2 +- 9 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/Markdig.Tests/TestParagraphParser.cs b/src/Markdig.Tests/TestParagraphParser.cs index 0db6cf38a..8b9781774 100644 --- a/src/Markdig.Tests/TestParagraphParser.cs +++ b/src/Markdig.Tests/TestParagraphParser.cs @@ -47,5 +47,28 @@ public void TestNewLinesBeforeAndAfter() Assert.AreEqual(markdown, sw.ToString()); } + + private void RoundTrip(string markdown) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + + [Test] + public void TestNewLinesBeforeAndAfter2() + { + RoundTrip("\n# H1\n\nLine1"); + RoundTrip("\n# H1\n\nLine1\n"); + RoundTrip("\n# H1\n\nLine1\n\n"); + RoundTrip("\n\n# H1\n\nLine1\n\n"); + RoundTrip("\n\n# H1\nLine1\n\n"); + } } } diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index 362eb9622..1906e0a73 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -33,6 +33,11 @@ public LineReader(string text) /// public int SourcePosition { get; private set; } + public bool IsEndOfFile() + { + return SourcePosition > _text.Length; + } + /// /// Reads a new line from the underlying and update the for the next line. /// diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 8ae35bf39..b52be90ea 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -34,6 +34,13 @@ private BlockProcessor(BlockProcessor root) NewBlocks = new Stack(); } + internal List UseLinesBefore() + { + var beforeLines = BeforeLines; + BeforeLines = null; + return beforeLines; + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index b651cfa11..81f7c0378 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -78,7 +78,9 @@ public override BlockState TryOpen(BlockProcessor processor) HeaderChar = matchingChar, Level = leadingCount, Column = column, - Span = { Start = sourcePosition } + Span = { Start = sourcePosition }, + BeforeWhitespace = processor.BeforeWhitespace.ToString(), + LinesBefore = processor.UseLinesBefore() }; processor.NewBlocks.Push(headingBlock); processor.GoToColumn(column + leadingCount + 1); diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 98fa28b43..500d57674 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -114,10 +114,14 @@ private void ProcessBlocks() // If this is the end of file and the last line is empty if (lineText.Text is null) { - var lastBlock = blockProcessor.LastBlock; - if (lastBlock != null) + if (!lineReader.IsEndOfFile()) { - lastBlock.LinesAfter = blockProcessor.BeforeLines; + var lastBlock = blockProcessor.LastBlock; + if (lastBlock != null) + { + lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); + lastBlock.LinesAfter.Add(lineText); + } } break; } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index bd0ee0d5f..121a6db40 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -22,15 +22,14 @@ public override BlockState TryOpen(BlockProcessor processor) { return BlockState.None; } - var linesBefore = processor.BeforeLines; - processor.BeforeLines = null; + // We continue trying to match by default processor.NewBlocks.Push(new ParagraphBlock(this) { Column = processor.Column, Span = new SourceSpan(processor.Line.Start, processor.Line.End), BeforeWhitespace = processor.BeforeWhitespace.ToString(), - LinesBefore = linesBefore + LinesBefore = processor.UseLinesBefore() }); return BlockState.Continue; } diff --git a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs index 100901781..249f8aca6 100644 --- a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs @@ -23,14 +23,19 @@ public class HeadingRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) { + renderer.RenderLinesBefore(obj); + var headingText = obj.Level > 0 && obj.Level <= 6 ? HeadingTexts[obj.Level - 1] : new string('#', obj.Level); + renderer.Write(obj.BeforeWhitespace); renderer.Write(headingText).Write(' '); renderer.WriteLeafInline(obj); - renderer.FinishBlock(renderer.Options.EmptyLineAfterHeading); + renderer.FinishBlock(); + + renderer.RenderLinesAfter(obj); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index 456eaf195..b83a4fa9e 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -50,15 +50,11 @@ public NormalizeRenderer(TextWriter writer, NormalizeOptions options = null) : b public bool CompactParagraph { get; set; } - public void FinishBlock(bool emptyLine) + public void FinishBlock(bool derp = false) { if (!IsLastInContainer) { WriteLine(); - if (emptyLine) - { - WriteLine(); - } } } @@ -158,5 +154,27 @@ public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfL } return this; } + + public void RenderLinesBefore(Block block) + { + if (block.LinesBefore != null) + { + foreach (var line in block.LinesBefore) + { + WriteLine(line.ToString()); + } + } + } + + public void RenderLinesAfter(Block block) + { + if (block.LinesAfter != null) + { + foreach (var line in block.LinesAfter) + { + WriteLine(line.ToString()); + } + } + } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 38ff2e946..df9c4bbf7 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -24,7 +24,7 @@ protected override void Write(NormalizeRenderer renderer, ParagraphBlock obj) renderer.Write(obj.BeforeWhitespace); renderer.WriteLeafInline(obj); - renderer.WriteLine(); + renderer.FinishBlock(); if (obj.LinesAfter != null) { From 0234d60d74c5b2c7892d794a7447f25b7ccb2b50 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 8 Aug 2020 18:25:08 +0200 Subject: [PATCH 006/120] fix broken whitespace calculation --- src/Markdig.Tests/TestParagraphParser.cs | 1 + src/Markdig/Parsers/BlockProcessor.cs | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Markdig.Tests/TestParagraphParser.cs b/src/Markdig.Tests/TestParagraphParser.cs index 8b9781774..27e83a34e 100644 --- a/src/Markdig.Tests/TestParagraphParser.cs +++ b/src/Markdig.Tests/TestParagraphParser.cs @@ -64,6 +64,7 @@ private void RoundTrip(string markdown) [Test] public void TestNewLinesBeforeAndAfter2() { + RoundTrip("paragraph"); RoundTrip("\n# H1\n\nLine1"); RoundTrip("\n# H1\n\nLine1\n"); RoundTrip("\n# H1\n\nLine1\n\n"); diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index b52be90ea..9ff257225 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -19,7 +19,6 @@ public class BlockProcessor private int currentStackIndex; private readonly BlockParserStateCache parserStateCache; private int originalLineStart = 0; - private List beforeLines; private BlockProcessor(BlockProcessor root) { @@ -174,11 +173,11 @@ public StringSlice BeforeWhitespace get { // TODO: make lazy - return new StringSlice(Line.Text, StartBeforeIndent, ColumnBeforeIndent); + return new StringSlice(Line.Text, StartBeforeIndent, ColumnBeforeIndent - 1); } } - public List BeforeLines { get => beforeLines; set => beforeLines = value; } + public List BeforeLines { get; set; } /// /// Get the current Container that is currently opened @@ -725,7 +724,7 @@ private void TryOpenBlocks() private void RestartBeforeLines() { - beforeLines = null; + BeforeLines = null; } /// From f78b5c83cd95f52e4580bf5c5cc4454c42eb3c92 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 17:07:10 +0200 Subject: [PATCH 007/120] handle whitespace before and after paragraphs correctly --- src/Markdig.Tests/Markdig.Tests.csproj | 1 + src/Markdig.Tests/TestCst.cs | 366 ++++++++++++++++++ src/Markdig.Tests/TestInlineLinkParser.cs | 52 --- src/Markdig.Tests/TestParagraphParser.cs | 75 ---- src/Markdig/Helpers/LinkHelper.cs | 2 +- src/Markdig/Helpers/StringSlice.cs | 7 + src/Markdig/Parsers/BlockProcessor.cs | 24 +- src/Markdig/Parsers/HeadingBlockParser.cs | 2 +- .../Parsers/Inlines/LiteralInlineParser.cs | 18 +- src/Markdig/Parsers/ListBlockParser.cs | 3 + src/Markdig/Parsers/ParagraphBlockParser.cs | 6 +- src/Markdig/Parsers/QuoteBlockParser.cs | 8 + .../Parsers/UnorderedListItemParser.cs | 1 + .../Renderers/Normalize/ListRenderer.cs | 25 +- .../Renderers/Normalize/ParagraphRenderer.cs | 26 +- .../Renderers/Normalize/QuoteBlockRenderer.cs | 11 +- src/Markdig/Syntax/Block.cs | 3 +- src/Markdig/Syntax/LeafBlock.cs | 9 +- src/Markdig/Syntax/QuoteBlock.cs | 5 + 19 files changed, 461 insertions(+), 183 deletions(-) create mode 100644 src/Markdig.Tests/TestCst.cs delete mode 100644 src/Markdig.Tests/TestInlineLinkParser.cs delete mode 100644 src/Markdig.Tests/TestParagraphParser.cs diff --git a/src/Markdig.Tests/Markdig.Tests.csproj b/src/Markdig.Tests/Markdig.Tests.csproj index d5c885ce7..5ec9999b6 100644 --- a/src/Markdig.Tests/Markdig.Tests.csproj +++ b/src/Markdig.Tests/Markdig.Tests.csproj @@ -12,6 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs new file mode 100644 index 000000000..b8887409f --- /dev/null +++ b/src/Markdig.Tests/TestCst.cs @@ -0,0 +1,366 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using NUnit.Framework; +using System.IO; + +/// +/// General notes +/// - whitespace can occur before, between and after symbols +/// +namespace Markdig.Tests +{ + [TestFixture] + public class TestCst + { + [Test] + public void TestWhitespaceBefore() + { + var markdown = " This is a paragraph. "; + + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as ParagraphBlock; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + + [Test] + public void TestNewLinesBeforeAndAfter() + { + var markdown = " \n \nLine2\n\nLine1\n\n"; + //var markdown = "\r\nLine2\r\n\r\nLine1\r\n\r\n"; + + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var paragraphBlock = markdownDocument[0] as ParagraphBlock; + + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + + private void RoundTrip(string markdown) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + + [TestCase("p")] + [TestCase(" p")] + [TestCase("p ")] + [TestCase(" p ")] + + [TestCase("p\np")] + [TestCase(" p\np")] + [TestCase("p \np")] + [TestCase(" p \np")] + + [TestCase("p\n p")] + [TestCase(" p\n p")] + [TestCase("p \n p")] + [TestCase(" p \n p")] + + [TestCase("p\np ")] + [TestCase(" p\np ")] + [TestCase("p \np ")] + [TestCase(" p \np ")] + + [TestCase("p\n\n p ")] + [TestCase(" p\n\n p ")] + [TestCase("p \n\n p ")] + [TestCase(" p \n\n p ")] + + [TestCase("p\n\np")] + [TestCase(" p\n\np")] + [TestCase("p \n\np")] + [TestCase(" p \n\np")] + + [TestCase("p\n\n p")] + [TestCase(" p\n\n p")] + [TestCase("p \n\n p")] + [TestCase(" p \n\n p")] + + [TestCase("p\n\np ")] + [TestCase(" p\n\np ")] + [TestCase("p \n\np ")] + [TestCase(" p \n\np ")] + + [TestCase("p\n\n p ")] + [TestCase(" p\n\n p ")] + [TestCase("p \n\n p ")] + [TestCase(" p \n\n p ")] + public void TestParagraph(string value) + { + RoundTrip(value); + } + + [Test] + public void TestNewLinesBeforeAndAfter2() + { + RoundTrip("\n# H1\n\nLine1"); + RoundTrip("\n# H1\n\nLine1\n"); + RoundTrip("\n# H1\n\nLine1\n\n"); + RoundTrip("\n\n# H1\n\nLine1\n\n"); + RoundTrip("\n\n# H1\nLine1\n\n"); + RoundTrip("\n\n# H1\nLine1\n\n"); + } + + // i = item + [TestCase("- i1")] + [TestCase("- i1 ")] + [TestCase("- i1\n- i2")] + [TestCase("- i1\n - i2")] + [TestCase("- i1\n - i1.1\n - i1.2")] + public void TestList(string value) + { + RoundTrip(value); + } + + + [Test] + public void TestImage() + { + RoundTrip(" ![description](http://example.com)"); + RoundTrip("paragraph ![description](http://example.com)"); + } + + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q1\n - > q2")] + public void TestListItem_BlockQuote(string value) + { + RoundTrip(value); + } + + [TestCase(">quote")] + [TestCase("> quote")] + [TestCase("> quote")] + [TestCase(" > quote")] + public void TestBlockQuote(string value) + { + RoundTrip(value); + } + + [Test] + public void TestBlockQuote() + { + //RoundTrip("- >quote"); // par in qb in l in li + // 3ws? - ws 3ws? q ws? p + // 3ws? lb + // - li + // ws li + // 3ws? qb + // q qb + // ws? qb + // p p + //RoundTrip(" - > quote"); // par in qb in l in li + } + + /// A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content. + [TestCase(" code")] + [TestCase(" code")] + public void TestImplicitCodeBlock(string value) + { + RoundTrip(value); + } + + [TestCase("[](b)")] + [TestCase(" [](b)")] + [TestCase("[](b) ")] + [TestCase(" [](b) ")] + + [TestCase("[a](b)")] + [TestCase(" [a](b)")] + [TestCase("[a](b) ")] + [TestCase(" [a](b) ")] + + [TestCase("[ a](b)")] + [TestCase(" [ a](b)")] + [TestCase("[ a](b) ")] + [TestCase(" [ a](b) ")] + + [TestCase("[a ](b)")] + [TestCase(" [a ](b)")] + [TestCase("[a ](b) ")] + [TestCase(" [a ](b) ")] + + [TestCase("[ a ](b)")] + [TestCase(" [ a ](b)")] + [TestCase("[ a ](b) ")] + [TestCase(" [ a ](b) ")] + + // below cases are required for a full CST but not have low prio for impl + //[TestCase("[]( b)")] + //[TestCase(" []( b)")] + //[TestCase("[]( b) ")] + //[TestCase(" []( b) ")] + + //[TestCase("[a]( b)")] + //[TestCase(" [a]( b)")] + //[TestCase("[a]( b) ")] + //[TestCase(" [a]( b) ")] + + //[TestCase("[ a]( b)")] + //[TestCase(" [ a]( b)")] + //[TestCase("[ a]( b) ")] + //[TestCase(" [ a]( b) ")] + + //[TestCase("[a ]( b)")] + //[TestCase(" [a ]( b)")] + //[TestCase("[a ]( b) ")] + //[TestCase(" [a ]( b) ")] + + //[TestCase("[ a ]( b)")] + //[TestCase(" [ a ]( b)")] + //[TestCase("[ a ]( b) ")] + //[TestCase(" [ a ]( b) ")] + + //[TestCase("[](b )")] + //[TestCase(" [](b )")] + //[TestCase("[](b ) ")] + //[TestCase(" [](b ) ")] + + //[TestCase("[a](b )")] + //[TestCase(" [a](b )")] + //[TestCase("[a](b ) ")] + //[TestCase(" [a](b ) ")] + + //[TestCase("[ a](b )")] + //[TestCase(" [ a](b )")] + //[TestCase("[ a](b ) ")] + //[TestCase(" [ a](b ) ")] + + //[TestCase("[a ](b )")] + //[TestCase(" [a ](b )")] + //[TestCase("[a ](b ) ")] + //[TestCase(" [a ](b ) ")] + + //[TestCase("[ a ](b )")] + //[TestCase(" [ a ](b )")] + //[TestCase("[ a ](b ) ")] + //[TestCase(" [ a ](b ) ")] + + //[TestCase("[]( b )")] + //[TestCase(" []( b )")] + //[TestCase("[]( b ) ")] + //[TestCase(" []( b ) ")] + + //[TestCase("[a]( b )")] + //[TestCase(" [a]( b )")] + //[TestCase("[a]( b ) ")] + //[TestCase(" [a]( b ) ")] + + //[TestCase("[ a]( b )")] + //[TestCase(" [ a]( b )")] + //[TestCase("[ a]( b ) ")] + //[TestCase(" [ a]( b ) ")] + + //[TestCase("[a ]( b )")] + //[TestCase(" [a ]( b )")] + //[TestCase("[a ]( b ) ")] + //[TestCase(" [a ]( b ) ")] + + //[TestCase("[ a ]( b )")] + //[TestCase(" [ a ]( b )")] + //[TestCase("[ a ]( b ) ")] + //[TestCase(" [ a ]( b ) ")] + public void TestInlineLink(string value) + { + RoundTrip(value); + } + + [TestCase("![](a)")] + [TestCase(" ![](a)")] + [TestCase("![](a) ")] + [TestCase(" ![](a) ")] + public void TestImage(string value) + { + RoundTrip(value); + } + + [TestCase("``")] + [TestCase(" ``")] + [TestCase("`` ")] + [TestCase(" `` ")] + + [TestCase("`a`")] + [TestCase(" `a`")] + [TestCase("`a` ")] + [TestCase(" `a` ")] + + [TestCase("` a`")] + [TestCase(" ` a`")] + [TestCase("` a` ")] + [TestCase(" ` a` ")] + + [TestCase("`a `")] + [TestCase(" `a `")] + [TestCase("`a ` ")] + [TestCase(" `a ` ")] + + /// : intentionally trimmed. TODO: decide on how to handle + //[TestCase("` a `")] + //[TestCase(" ` a `")] + //[TestCase("` a ` ")] + //[TestCase(" ` a ` ")] + public void TestCodeInline(string value) + { + RoundTrip(value); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + + [TestCase("< http://a>")] + [TestCase(" < http://a>")] + [TestCase("< http://a> ")] + [TestCase(" < http://a> ")] + + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + + [TestCase("< http://a >")] + [TestCase(" < http://a >")] + [TestCase("< http://a > ")] + [TestCase(" < http://a > ")] + public void TestAutolinkInline(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/TestInlineLinkParser.cs b/src/Markdig.Tests/TestInlineLinkParser.cs deleted file mode 100644 index 98e7abd1b..000000000 --- a/src/Markdig.Tests/TestInlineLinkParser.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Markdig.Renderers.Normalize; -using Markdig.Syntax; -using Markdig.Syntax.Inlines; -using NUnit.Framework; -using System.IO; - -namespace Markdig.Tests -{ - [TestFixture] - public class TestInlineLinkParser - { - [Test] - public void Test() - { - string markdown = " ![ description ]( http://example.com )"; - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as ParagraphBlock; - var containerInline = paragraphBlock.Inline as ContainerInline; - var linkInline = containerInline.FirstChild as LinkInline; - var description = linkInline.FirstChild as LiteralInline; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - [Test] - public void Test1() - { - string markdown = " ![description](http://example.com)"; - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as ParagraphBlock; - var containerInline = paragraphBlock.Inline as ContainerInline; - var linkInline = containerInline.FirstChild as LinkInline; - var description = linkInline.FirstChild as LiteralInline; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - } -} diff --git a/src/Markdig.Tests/TestParagraphParser.cs b/src/Markdig.Tests/TestParagraphParser.cs deleted file mode 100644 index 27e83a34e..000000000 --- a/src/Markdig.Tests/TestParagraphParser.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Markdig.Renderers.Normalize; -using Markdig.Syntax; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Markdig.Tests -{ - [TestFixture] - public class TestParagraphParser - { - [Test] - public void TestWhitespaceBefore() - { - var markdown = " This is a paragraph. "; - - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as ParagraphBlock; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - [Test] - public void TestNewLinesBeforeAndAfter() - { - var markdown = " \n \nLine2\n\nLine1\n\n"; - //var markdown = "\r\nLine2\r\n\r\nLine1\r\n\r\n"; - - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as ParagraphBlock; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - private void RoundTrip(string markdown) - { - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - [Test] - public void TestNewLinesBeforeAndAfter2() - { - RoundTrip("paragraph"); - RoundTrip("\n# H1\n\nLine1"); - RoundTrip("\n# H1\n\nLine1\n"); - RoundTrip("\n# H1\n\nLine1\n\n"); - RoundTrip("\n\n# H1\n\nLine1\n\n"); - RoundTrip("\n\n# H1\nLine1\n\n"); - } - } -} diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 22296e9b5..02aec0a9e 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -356,7 +356,7 @@ public static bool TryParseInlineLink(ref StringSlice text, out string link, out if (c == '(') { text.NextChar(); - text.TrimStart(); + text.TrimStart(); // this breaks whitespace before an uri var pos = text.Start; if (TryParseUrl(ref text, out link)) diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 786046d01..84c19d2ca 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -26,6 +26,7 @@ public StringSlice(string text) { Text = text; Start = 0; + ActualStart = 0; End = (Text?.Length ?? 0) - 1; } @@ -43,6 +44,7 @@ public StringSlice(string text, int start, int end) Text = text; Start = start; + ActualStart = start; End = end; } @@ -56,6 +58,11 @@ public StringSlice(string text, int start, int end) /// public int Start { readonly get; set; } + /// + /// Gets or sets the start position within . + /// + public int ActualStart { get; } + /// /// Gets or sets the end position (inclusive) within . /// diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 9ff257225..4923d799e 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -168,13 +168,14 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo /// private bool ContinueProcessingLine { get; set; } - public StringSlice BeforeWhitespace + public int WhitespaceStart { get; set; } + + public StringSlice PopBeforeWhitespace(int column) { - get - { - // TODO: make lazy - return new StringSlice(Line.Text, StartBeforeIndent, ColumnBeforeIndent - 1); - } + var stringSlice = new StringSlice(Line.Text, WhitespaceStart, column - 1); + //var stringSlice = new StringSlice(Line.Text, Line.Start + WhitespaceStart, Line.Start + column - 1); + WhitespaceStart = column; + return stringSlice; } public List BeforeLines { get; set; } @@ -267,13 +268,13 @@ public void ParseIndent() { Column = CharHelper.AddTab(Column); } - else if (c == ' ') - { - Column++; - } + //else if (c == ' ') + //{ + // Column++; + //} else { - break; + break; } c = Line.NextChar(); } @@ -883,6 +884,7 @@ private void ResetLine(StringSlice newLine) ColumnBeforeIndent = 0; StartBeforeIndent = Start; originalLineStart = newLine.Start; + WhitespaceStart = 0; } private void Reset() diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 81f7c0378..d8527302c 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -79,7 +79,7 @@ public override BlockState TryOpen(BlockProcessor processor) Level = leadingCount, Column = column, Span = { Start = sourcePosition }, - BeforeWhitespace = processor.BeforeWhitespace.ToString(), + BeforeWhitespace = processor.PopBeforeWhitespace(column), LinesBefore = processor.UseLinesBefore() }; processor.NewBlocks.Push(headingBlock); diff --git a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs index 68f87409c..7ec345eae 100644 --- a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs @@ -50,15 +50,15 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { // Remove line endings if the next char is a new line length = nextStart - slice.Start; - if (text[nextStart] == '\n') - { - int end = nextStart - 1; - while (length > 0 && text[end].IsSpace()) - { - length--; - end--; - } - } + //if (text[nextStart] == '\n') + //{ + // int end = nextStart - 1; + // while (length > 0 && text[end].IsSpace()) + // { + // length--; + // end--; + // } + //} } // The LiteralInlineParser is always matching (at least an empty string) diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index fdc34d8c1..7d8ce930c 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -211,6 +211,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) // Item starting with a blank line int columnWidth; + var beforeWhitespace = state.PopBeforeWhitespace(initColumn); // Do we have a blank line right after the bullet? if (c == '\0') @@ -257,12 +258,14 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) } } + state.WhitespaceStart += 2; int.TryParse(listInfo.OrderedStart, out int order); var newListItem = new ListItemBlock(this) { Column = initColumn, ColumnWidth = columnWidth, Order = order, + BeforeWhitespace = beforeWhitespace, Span = new SourceSpan(sourcePosition, sourceEndPosition) }; state.NewBlocks.Push(newListItem); diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 121a6db40..ee92fe217 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -28,7 +28,7 @@ public override BlockState TryOpen(BlockProcessor processor) { Column = processor.Column, Span = new SourceSpan(processor.Line.Start, processor.Line.End), - BeforeWhitespace = processor.BeforeWhitespace.ToString(), + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Column), LinesBefore = processor.UseLinesBefore() }); return BlockState.Continue; @@ -102,8 +102,10 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Span = new SourceSpan(paragraph.Span.Start, line.Start), Level = level, Lines = paragraph.Lines, + BeforeWhitespace = state.PopBeforeWhitespace(state.Column), + LinesBefore = state.UseLinesBefore() }; - heading.Lines.Trim(); + //heading.Lines.Trim(); // Remove the paragraph as a pending block state.NewBlocks.Push(heading); diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index a9d9bde9a..915bd8bbb 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -35,16 +35,24 @@ public override BlockState TryOpen(BlockProcessor processor) // A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space. var quoteChar = processor.CurrentChar; var c = processor.NextChar(); + bool hasSpaceAfterQuoteChar = false; + int whitespaceToAdd = 1; if (c.IsSpaceOrTab()) { processor.NextColumn(); + hasSpaceAfterQuoteChar = true; + whitespaceToAdd += 1; } + //beforeWhitespace.End -= 1 + (hasSpaceAfterQuoteChar ? 1 : 0); processor.NewBlocks.Push(new QuoteBlock(this) { QuoteChar = quoteChar, Column = column, Span = new SourceSpan(sourcePosition, processor.Line.End), + BeforeWhitespace = processor.PopBeforeWhitespace(column), + HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, }); + processor.WhitespaceStart += whitespaceToAdd; return BlockState.Continue; } diff --git a/src/Markdig/Parsers/UnorderedListItemParser.cs b/src/Markdig/Parsers/UnorderedListItemParser.cs index 5a8e20605..11b718315 100644 --- a/src/Markdig/Parsers/UnorderedListItemParser.cs +++ b/src/Markdig/Parsers/UnorderedListItemParser.cs @@ -22,6 +22,7 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out { result = new ListInfo(state.CurrentChar); state.NextChar(); + state.NextChar(); return true; } } diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 71b2c4749..16c068201 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -15,7 +15,7 @@ public class ListRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) { - renderer.EnsureLine(); + renderer.RenderLinesBefore(listBlock); var compact = renderer.CompactParagraph; renderer.CompactParagraph = !listBlock.IsLoose; if (listBlock.IsOrdered) @@ -30,12 +30,17 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) break; } } + var writeLine = false; for (var i = 0; i < listBlock.Count; i++) { var item = listBlock[i]; var listItem = (ListItemBlock) item; - renderer.EnsureLine(); + if (writeLine) + { + renderer.WriteLine(); + } + renderer.Write(listItem.BeforeWhitespace); renderer.Write(index.ToString(CultureInfo.InvariantCulture)); renderer.Write(listBlock.OrderedDelimiter); renderer.Write(' '); @@ -50,18 +55,25 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } if (i + 1 < listBlock.Count && listBlock.IsLoose) { - renderer.EnsureLine(); + //renderer.EnsureLine(); renderer.WriteLine(); } + writeLine = true; } } else { + var writeLine = false; for (var i = 0; i < listBlock.Count; i++) { + if (writeLine) + { + renderer.WriteLine(); + } var item = listBlock[i]; var listItem = (ListItemBlock) item; - renderer.EnsureLine(); + //renderer.EnsureLine(); + renderer.Write(listItem.BeforeWhitespace); renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); renderer.Write(' '); renderer.PushIndent(" "); @@ -69,14 +81,15 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) renderer.PopIndent(); if (i + 1 < listBlock.Count && listBlock.IsLoose) { - renderer.EnsureLine(); + //renderer.EnsureLine(); renderer.WriteLine(); } + writeLine = true; } } renderer.CompactParagraph = compact; - renderer.FinishBlock(true); + renderer.RenderLinesAfter(listBlock); } diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index df9c4bbf7..00ead23c2 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; +using System.Diagnostics; namespace Markdig.Renderers.Normalize { @@ -10,29 +11,16 @@ namespace Markdig.Renderers.Normalize /// A Normalize renderer for a . /// /// + [DebuggerDisplay("renderer.Writer.ToString()")] public class ParagraphRenderer : NormalizeObjectRenderer { - protected override void Write(NormalizeRenderer renderer, ParagraphBlock obj) + protected override void Write(NormalizeRenderer renderer, ParagraphBlock parapgraph) { - if (obj.LinesBefore != null) - { - foreach (var line in obj.LinesBefore) - { - renderer.WriteLine(line.ToString()); - } - } - - renderer.Write(obj.BeforeWhitespace); - renderer.WriteLeafInline(obj); + renderer.RenderLinesBefore(parapgraph); + renderer.Write(parapgraph.BeforeWhitespace); + renderer.WriteLeafInline(parapgraph); renderer.FinishBlock(); - - if (obj.LinesAfter != null) - { - foreach (var line in obj.LinesAfter) - { - renderer.WriteLine(line.ToString()); - } - } + renderer.RenderLinesAfter(parapgraph); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 345b99341..4f773e7af 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -14,12 +14,15 @@ public class QuoteBlockRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, QuoteBlock obj) { - var quoteIndent = renderer.Options.SpaceAfterQuoteBlock ? obj.QuoteChar + " " : obj.QuoteChar.ToString(); - renderer.PushIndent(quoteIndent); + renderer.Write(obj.BeforeWhitespace); + + var quoteIndent = obj.HasSpaceAfterQuoteChar ? obj.QuoteChar + " " : obj.QuoteChar.ToString(); + //renderer.PushIndent(quoteIndent); + renderer.Write(quoteIndent); renderer.WriteChildren(obj); - renderer.PopIndent(); + //renderer.PopIndent(); - renderer.FinishBlock(true); + renderer.FinishBlock(); } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index de22908b2..282c7d022 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -50,7 +50,8 @@ protected Block(BlockParser parser) /// public bool RemoveAfterProcessInlines { get; set; } - public string BeforeWhitespace { get; set; } + public StringSlice BeforeWhitespace { get; set; } + public StringSlice AfterWhitespace { get; set; } public List LinesBefore { get; set; } public List LinesAfter { get; internal set; } diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index dbc7bacf5..6cbb9c25c 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -54,8 +54,13 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi { Lines = new StringLineGroup(4, ProcessInlines); } - - var stringLine = new StringLine(ref slice, line, column, sourceLinePosition); + int c = column; + if (Lines.Count != 0) + { + // if not first line, preserve whitespace + c = 0; + } + var stringLine = new StringLine(ref slice, line, c, sourceLinePosition); // Regular case, we are not in the middle of a tab if (slice.CurrentChar != '\t' || !CharHelper.IsAcrossTab(column)) { diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index f2147856a..3ab29e2bc 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -24,5 +24,10 @@ public QuoteBlock(BlockParser parser) : base(parser) /// Gets or sets the quote character (usually `>`) /// public char QuoteChar { get; set; } + + /// + /// True if a space is parsed between the > character and the paragraph + /// + public bool HasSpaceAfterQuoteChar { get; internal set; } } } \ No newline at end of file From 0d86a9320003d7d595ebfdc2a3ad64bc7e691016 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 17:58:58 +0200 Subject: [PATCH 008/120] fix block nodes --- src/Markdig.Tests/TestCst.cs | 43 ++++++++++++++++++- src/Markdig/Parsers/FencedCodeBlockParser.cs | 6 ++- src/Markdig/Parsers/NumberedListItemParser.cs | 1 + src/Markdig/Parsers/OrderedListItemParser.cs | 1 + src/Markdig/Parsers/ParagraphBlockParser.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 1 + .../Renderers/Normalize/CodeBlockRenderer.cs | 2 + .../Renderers/Normalize/QuoteBlockRenderer.cs | 10 +++-- 8 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index b8887409f..fa0a425d1 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -106,6 +106,13 @@ private void RoundTrip(string markdown) [TestCase(" p\n\n p ")] [TestCase("p \n\n p ")] [TestCase(" p \n\n p ")] + + // special cases + [TestCase(" p \n\n\n\n p \n\n")] + [TestCase("\np")] + [TestCase("\n\np")] + [TestCase("p\n")] + [TestCase("p\n\n")] public void TestParagraph(string value) { RoundTrip(value); @@ -128,11 +135,19 @@ public void TestNewLinesBeforeAndAfter2() [TestCase("- i1\n- i2")] [TestCase("- i1\n - i2")] [TestCase("- i1\n - i1.1\n - i1.2")] - public void TestList(string value) + public void TestUnorederedList(string value) { RoundTrip(value); } + [TestCase("1. i")] + [TestCase("1. i")] + [TestCase("1. i ")] + [TestCase("1. i ")] + public void TestOrderedList(string value) + { + RoundTrip(value); + } [Test] public void TestImage() @@ -158,7 +173,7 @@ public void TestImage() [TestCase(" - > q")] [TestCase(" - > q")] [TestCase(" - > q1\n - > q2")] - public void TestListItem_BlockQuote(string value) + public void TestUnorderedListItem_BlockQuote(string value) { RoundTrip(value); } @@ -172,6 +187,16 @@ public void TestBlockQuote(string value) RoundTrip(value); } + [TestCase("\n> q")] + [TestCase("\n> q\n")] + [TestCase("\n> q\n\n")] + [TestCase("> q\n\np")] + [TestCase("p\n\n> q\n\n# h")] + public void TestBlockQuote_Paragraph(string value) + { + RoundTrip(value); + } + [Test] public void TestBlockQuote() { @@ -362,5 +387,19 @@ public void TestAutolinkInline(string value) { RoundTrip(value); } + + [TestCase("```\nc\n```")] + [TestCase("\n```\nc\n```")] + [TestCase("\n\n```\nc\n```")] + [TestCase("```\nc\n```\n")] + [TestCase("```\nc\n```\n\n")] + [TestCase("\n```\nc\n```\n")] + [TestCase("\n```\nc\n```\n\n")] + [TestCase("\n\n```\nc\n```\n")] + [TestCase("\n\n```\nc\n```\n\n")] + public void TestCodeBlock(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index 41c8937be..694837c57 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -26,7 +26,11 @@ public FencedCodeBlockParser() protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) { - return new FencedCodeBlock(this) {IndentCount = processor.Indent}; + return new FencedCodeBlock(this) + { + IndentCount = processor.Indent, + LinesBefore = processor.UseLinesBefore(), + }; } public override BlockState TryContinue(BlockProcessor processor, Block block) diff --git a/src/Markdig/Parsers/NumberedListItemParser.cs b/src/Markdig/Parsers/NumberedListItemParser.cs index 07dc0ed2d..2650bd2fb 100644 --- a/src/Markdig/Parsers/NumberedListItemParser.cs +++ b/src/Markdig/Parsers/NumberedListItemParser.cs @@ -40,6 +40,7 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out { startChar = endChar; } + state.NextChar(); c = state.NextChar(); countDigit++; } diff --git a/src/Markdig/Parsers/OrderedListItemParser.cs b/src/Markdig/Parsers/OrderedListItemParser.cs index 23d694b83..2e20796e1 100644 --- a/src/Markdig/Parsers/OrderedListItemParser.cs +++ b/src/Markdig/Parsers/OrderedListItemParser.cs @@ -37,6 +37,7 @@ protected bool TryParseDelimiter(BlockProcessor state, out char orderedDelimiter { if (delimiter == orderedDelimiter) { + state.NextChar(); state.NextChar(); return true; } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index ee92fe217..bc12da7a8 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -103,7 +103,7 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Level = level, Lines = paragraph.Lines, BeforeWhitespace = state.PopBeforeWhitespace(state.Column), - LinesBefore = state.UseLinesBefore() + LinesBefore = state.UseLinesBefore(), }; //heading.Lines.Trim(); diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 915bd8bbb..066cfe95d 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -51,6 +51,7 @@ public override BlockState TryOpen(BlockProcessor processor) Span = new SourceSpan(sourcePosition, processor.Line.End), BeforeWhitespace = processor.PopBeforeWhitespace(column), HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, + LinesBefore = processor.UseLinesBefore() }); processor.WhitespaceStart += whitespaceToAdd; return BlockState.Continue; diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 4a6f11e93..6aaa495dc 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -18,6 +18,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) { if (obj is FencedCodeBlock fencedCodeBlock) { + renderer.RenderLinesBefore(obj); var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.FencedCharCount); renderer.Write(opening); @@ -55,6 +56,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.WriteLeafRawLines(obj, true); renderer.Write(opening); + renderer.RenderLinesAfter(obj); } else { diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 4f773e7af..43f341b9e 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -12,17 +12,19 @@ namespace Markdig.Renderers.Normalize /// public class QuoteBlockRenderer : NormalizeObjectRenderer { - protected override void Write(NormalizeRenderer renderer, QuoteBlock obj) + protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) { - renderer.Write(obj.BeforeWhitespace); + renderer.RenderLinesBefore(quoteBlock); + renderer.Write(quoteBlock.BeforeWhitespace); - var quoteIndent = obj.HasSpaceAfterQuoteChar ? obj.QuoteChar + " " : obj.QuoteChar.ToString(); + var quoteIndent = quoteBlock.HasSpaceAfterQuoteChar ? quoteBlock.QuoteChar + " " : quoteBlock.QuoteChar.ToString(); //renderer.PushIndent(quoteIndent); renderer.Write(quoteIndent); - renderer.WriteChildren(obj); + renderer.WriteChildren(quoteBlock); //renderer.PopIndent(); renderer.FinishBlock(); + renderer.RenderLinesAfter(quoteBlock); } } } \ No newline at end of file From cadbc67825e9ad78f2bce5d02b8764ec276af773 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 18:55:54 +0200 Subject: [PATCH 009/120] fix newline between blocks --- src/Markdig.Tests/TestCst.cs | 6 ++++++ src/Markdig/Parsers/BlockProcessor.cs | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index fa0a425d1..8e6650bbf 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -197,6 +197,12 @@ public void TestBlockQuote_Paragraph(string value) RoundTrip(value); } + [TestCase("> q\n\n# h\n")] + public void TestBlockQuote_Header(string value) + { + RoundTrip(value); + } + [Test] public void TestBlockQuote() { diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 4923d799e..ebeae0967 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -166,7 +166,15 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo /// /// Gets or sets a value indicating whether to continue processing the current line. /// - private bool ContinueProcessingLine { get; set; } + private bool _cpl; + private bool ContinueProcessingLine { get => _cpl; + set + { + + + _cpl = value; + } + } public int WhitespaceStart { get; set; } @@ -663,6 +671,12 @@ private void TryContinueBlocks() if (result == BlockState.BreakDiscard) { + if (Line.IsEmpty) + { + BeforeLines ??= new List(); + Line.Start = StartBeforeIndent; + BeforeLines.Add(Line); + } ContinueProcessingLine = false; break; } From acf2ba950265d3c2f39449d5e96702db98bea52e Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 19:23:11 +0200 Subject: [PATCH 010/120] allow CodeInline with multiple delimiter characters --- src/Markdig.Tests/TestCst.cs | 10 +++++++ .../Parsers/Inlines/CodeInlineParser.cs | 5 +++- .../Normalize/Inlines/CodeInlineRenderer.cs | 28 +++++++++---------- src/Markdig/Syntax/Inlines/CodeInline.cs | 6 ++++ 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 8e6650bbf..6137de8e9 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -360,6 +360,16 @@ public void TestImage(string value) [TestCase("`a ` ")] [TestCase(" `a ` ")] + [TestCase("``a``")] + [TestCase("- ```a```")] + [TestCase("p ```a``` p")] + + // broken + //[TestCase("```a```")] + [TestCase("```a``` p")] + //[TestCase("p\n\n```a``` p")] + //[TestCase("```a``` p\n```a``` p")] + /// : intentionally trimmed. TODO: decide on how to handle //[TestCase("` a `")] //[TestCase(" ` a `")] diff --git a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs index 16cb1abec..00cbff4ee 100644 --- a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs @@ -5,6 +5,7 @@ using Markdig.Helpers; using Markdig.Syntax; using Markdig.Syntax.Inlines; +using System; namespace Markdig.Parsers.Inlines { @@ -100,13 +101,15 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) content = builder.ToString(); } + int delimiterCount = Math.Min(openSticks, closeSticks); processor.Inline = new CodeInline() { Delimiter = match, Content = content, Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), Line = line, - Column = column + Column = column, + DelimiterCount = delimiterCount }; isMatching = true; } diff --git a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs index 46fd6b671..e7a8aab9a 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs @@ -15,22 +15,22 @@ public class CodeInlineRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, CodeInline obj) { var delimiterCount = 0; - for (var i = 0; i < obj.Content.Length; i++) - { - var index = obj.Content.IndexOf(obj.Delimiter, i); - if (index == -1) break; + //for (var i = 0; i < obj.Content.Length; i++) + //{ + // var index = obj.Content.IndexOf(obj.Delimiter, i); + // if (index == -1) break; - var count = 1; - for (i = index + 1; i < obj.Content.Length; i++) - { - if (obj.Content[i] == obj.Delimiter) count++; - else break; - } + // var count = 1; + // for (i = index + 1; i < obj.Content.Length; i++) + // { + // if (obj.Content[i] == obj.Delimiter) count++; + // else break; + // } - if (delimiterCount < count) - delimiterCount = count; - } - var delimiterRun = new string(obj.Delimiter, delimiterCount + 1); + // if (delimiterCount < count) + // delimiterCount = count; + //} + var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); renderer.Write(delimiterRun); if (obj.Content.Length != 0) { diff --git a/src/Markdig/Syntax/Inlines/CodeInline.cs b/src/Markdig/Syntax/Inlines/CodeInline.cs index c445fa382..1aae6a7b0 100644 --- a/src/Markdig/Syntax/Inlines/CodeInline.cs +++ b/src/Markdig/Syntax/Inlines/CodeInline.cs @@ -18,9 +18,15 @@ public class CodeInline : LeafInline /// public char Delimiter { get; set; } + /// + /// Gets or sets the amount of delimiter characters used + /// + public int DelimiterCount { get; set; } + /// /// Gets or sets the content of the span. /// public string Content { get; set; } + } } \ No newline at end of file From 976855a4c395fd36bf148663df5b5527de01c69d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 21:23:28 +0200 Subject: [PATCH 011/120] implement TheamticBreak renderer --- src/Markdig.Tests/TestCst.cs | 50 +++++++++++++++++-- src/Markdig/Parsers/HeadingBlockParser.cs | 2 +- .../Normalize/Inlines/LinkInlineRenderer.cs | 2 +- .../Normalize/ThematicBreakRenderer.cs | 6 +-- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 6137de8e9..2c7c7add2 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -132,6 +132,7 @@ public void TestNewLinesBeforeAndAfter2() // i = item [TestCase("- i1")] [TestCase("- i1 ")] + [TestCase("- i1\n")] [TestCase("- i1\n- i2")] [TestCase("- i1\n - i2")] [TestCase("- i1\n - i1.1\n - i1.2")] @@ -140,6 +141,13 @@ public void TestUnorederedList(string value) RoundTrip(value); } + [TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline + [TestCase("- i1\n\n\np\n")] + public void TestUnorderedList_Paragraph(string value) + { + RoundTrip(value); + } + [TestCase("1. i")] [TestCase("1. i")] [TestCase("1. i ")] @@ -149,11 +157,18 @@ public void TestOrderedList(string value) RoundTrip(value); } - [Test] - public void TestImage() + [TestCase(" ![description](http://example.com)")] + [TestCase("paragraph ![description](http://example.com)")] + public void TestImage2(string value) { - RoundTrip(" ![description](http://example.com)"); - RoundTrip("paragraph ![description](http://example.com)"); + RoundTrip(value); + } + + [TestCase("# h")] + [TestCase("# h ")] + public void TestHeading(string value) + { + RoundTrip(value); } [TestCase("- > q")] @@ -182,11 +197,28 @@ public void TestUnorderedListItem_BlockQuote(string value) [TestCase("> quote")] [TestCase("> quote")] [TestCase(" > quote")] + [TestCase(">q\n>\n>q")] public void TestBlockQuote(string value) { RoundTrip(value); } + [TestCase("---")] + [TestCase(" ---")] + [TestCase(" ---")] + [TestCase(" ---")] + [TestCase("--- ")] + [TestCase(" --- ")] + [TestCase(" --- ")] + [TestCase(" --- ")] + [TestCase("---\np")] + [TestCase("---\n\np")] + [TestCase("p\n---\n\np")] + public void TestThematicBreak(string value) + { + RoundTrip(value); + } + [TestCase("\n> q")] [TestCase("\n> q\n")] [TestCase("\n> q\n\n")] @@ -203,6 +235,12 @@ public void TestBlockQuote_Header(string value) RoundTrip(value); } + [TestCase(">- i1\n>- i2\n")] + public void TestBlockQuote_ListBlock(string value) + { + RoundTrip(value); + } + [Test] public void TestBlockQuote() { @@ -226,6 +264,9 @@ public void TestImplicitCodeBlock(string value) RoundTrip(value); } + [TestCase("[a]")] // TODO: this is not a link but a paragraph + [TestCase("[a]()")] + [TestCase("[](b)")] [TestCase(" [](b)")] [TestCase("[](b) ")] @@ -367,6 +408,7 @@ public void TestImage(string value) // broken //[TestCase("```a```")] [TestCase("```a``` p")] + [TestCase("```a`b`c```")] //[TestCase("p\n\n```a``` p")] //[TestCase("```a``` p\n```a``` p")] diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index d8527302c..4fff52fd4 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -139,7 +139,7 @@ public override BlockState TryOpen(BlockProcessor processor) public override bool Close(BlockProcessor processor, Block block) { var heading = (HeadingBlock)block; - heading.Lines.Trim(); + //heading.Lines.Trim(); return true; } } diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index 9b763b155..9502f9729 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -48,7 +48,7 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) } else { - if (!string.IsNullOrEmpty(link.Url)) + if (link.Url != null) { renderer.Write('(').Write(link.Url); diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 1202b098d..3ab4a8346 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -14,9 +14,9 @@ public class ThematicBreakRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj) { - renderer.WriteLine(new string(obj.ThematicChar, obj.ThematicCharCount)); - - renderer.FinishBlock(renderer.Options.EmptyLineAfterThematicBreak); + renderer.RenderLinesBefore(obj); + renderer.Write(new string(obj.ThematicChar, obj.ThematicCharCount)); + renderer.RenderLinesAfter(obj); } } } \ No newline at end of file From 6792bffb5e814cf6674787b35d7ca2ba53b0aba5 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 21:39:53 +0200 Subject: [PATCH 012/120] better thematic break handling --- src/Markdig.Tests/TestCst.cs | 8 +++++--- src/Markdig/Parsers/ThematicBreakParser.cs | 3 ++- src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 2c7c7add2..b7fbe30db 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -203,17 +203,19 @@ public void TestBlockQuote(string value) RoundTrip(value); } - [TestCase("---")] + //[TestCase("---")] [TestCase(" ---")] [TestCase(" ---")] [TestCase(" ---")] - [TestCase("--- ")] + //[TestCase("--- ")] [TestCase(" --- ")] [TestCase(" --- ")] [TestCase(" --- ")] [TestCase("---\np")] [TestCase("---\n\np")] - [TestCase("p\n---\n\np")] + [TestCase("---\n# h")] + //[TestCase("p\n\n---")] + /// Note: "p\n---" is parsed as setext heading public void TestThematicBreak(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index 2457f3225..8481bd20c 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -91,7 +91,8 @@ public override BlockState TryOpen(BlockProcessor processor) Column = processor.Column, Span = new SourceSpan(startPosition, line.End), ThematicChar = breakChar, - ThematicCharCount = breakCharCount + ThematicCharCount = breakCharCount, + LinesBefore = processor.UseLinesBefore(), }); return BlockState.BreakDiscard; } diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 3ab4a8346..7726aff43 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -15,7 +15,10 @@ public class ThematicBreakRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj) { renderer.RenderLinesBefore(obj); - renderer.Write(new string(obj.ThematicChar, obj.ThematicCharCount)); + + // for now, render always a newline + // TODO: only render a newline when not last line + renderer.WriteLine(new string(obj.ThematicChar, obj.ThematicCharCount)); renderer.RenderLinesAfter(obj); } } From 37af8f8ecbc1f432f402b6a2017794ae3352cc1a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 22:03:34 +0200 Subject: [PATCH 013/120] implement newline with HtmlBlock --- src/Markdig.Tests/TestCst.cs | 9 +++++++++ src/Markdig.Tests/TestFencedCodeBlock.cs | 4 ++-- src/Markdig.Tests/TestNormalize.cs | 2 +- .../Extensions/CustomContainers/CustomContainer.cs | 4 +++- src/Markdig/Parsers/FencedBlockParserBase.cs | 10 ++++++---- src/Markdig/Parsers/HtmlBlockParser.cs | 3 ++- src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs | 5 +++-- src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs | 4 +++- src/Markdig/Syntax/FencedCodeBlock.cs | 7 ++++++- src/Markdig/Syntax/IFencedBlock.cs | 7 ++++++- 10 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index b7fbe30db..b54a6c5fe 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -457,9 +457,18 @@ public void TestAutolinkInline(string value) [TestCase("\n```\nc\n```\n\n")] [TestCase("\n\n```\nc\n```\n")] [TestCase("\n\n```\nc\n```\n\n")] + + [TestCase("```\nc\n````")] public void TestCodeBlock(string value) { RoundTrip(value); } + + [TestCase("p\n\n
\n")] + [TestCase("
\n\n# h")] + public void TestHtml(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/TestFencedCodeBlock.cs b/src/Markdig.Tests/TestFencedCodeBlock.cs index fa7c1fcf7..e18d1c4f7 100644 --- a/src/Markdig.Tests/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/TestFencedCodeBlock.cs @@ -22,7 +22,7 @@ public void CstInfoParser_ParsesHappyFlow() var fencedCodeBlock = markdownDocument[0] as FencedCodeBlock; Assert.AreEqual('`', fencedCodeBlock.FencedChar); - Assert.AreEqual(3, fencedCodeBlock.FencedCharCount); + Assert.AreEqual(3, fencedCodeBlock.OpeningFencedCharCount); Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterFencedChar); Assert.AreEqual("derpy", fencedCodeBlock.Info); Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterInfo); @@ -41,7 +41,7 @@ public void CstInfoParser_ParsesComplicatedArguments() var fencedCodeBlock = markdownDocument[0] as FencedCodeBlock; Assert.AreEqual('`', fencedCodeBlock.FencedChar); - Assert.AreEqual(3, fencedCodeBlock.FencedCharCount); + Assert.AreEqual(3, fencedCodeBlock.OpeningFencedCharCount); Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterFencedChar); Assert.AreEqual("derpy", fencedCodeBlock.Info); Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterInfo); diff --git a/src/Markdig.Tests/TestNormalize.cs b/src/Markdig.Tests/TestNormalize.cs index 273d8914f..0007f3521 100644 --- a/src/Markdig.Tests/TestNormalize.cs +++ b/src/Markdig.Tests/TestNormalize.cs @@ -21,7 +21,7 @@ public void SyntaxCodeBlock() AssertSyntax("````csharp\npublic void HelloWorld()\n{\n}\n````", new FencedCodeBlock(null) { FencedChar = '`', - FencedCharCount = 4, + OpeningFencedCharCount = 4, Info = "csharp", Lines = new StringLineGroup(4) { diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index dab05c618..a3eb5aab9 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -33,7 +33,9 @@ public CustomContainer(BlockParser parser) : base(parser) public string Arguments { get; set; } public string WhitespaceAfterArguments { get; set; } - public int FencedCharCount { get; set; } + public int OpeningFencedCharCount { get; set; } + + public int ClosingFencedCharCount { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 8463d26d3..b56502bed 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -269,7 +269,7 @@ public override BlockState TryOpen(BlockProcessor processor) { fenced.Column = processor.Column; fenced.FencedChar = matchChar; - fenced.FencedCharCount = count; + fenced.OpeningFencedCharCount = count; fenced.Span.Start = processor.Start; fenced.Span.End = line.Start; }; @@ -301,19 +301,21 @@ public override BlockState TryOpen(BlockProcessor processor) public override BlockState TryContinue(BlockProcessor processor, Block block) { var fence = (IFencedBlock)block; - var count = fence.FencedCharCount; + var openingCount = fence.OpeningFencedCharCount; // Match if we have a closing fence var line = processor.Line; - count -= line.CountAndSkipChar(fence.FencedChar); + var closingCount = line.CountAndSkipChar(fence.FencedChar); + var diff = openingCount - closingCount; char c = line.CurrentChar; // If we have a closing fence, close it and discard the current line // The line must contain only fence opening character followed only by whitespaces. - if (count <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) + if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) { block.UpdateSpanEnd(line.Start - 1); + (block as IFencedBlock).ClosingFencedCharCount = closingCount; // Don't keep the last line return BlockState.BreakDiscard; diff --git a/src/Markdig/Parsers/HtmlBlockParser.cs b/src/Markdig/Parsers/HtmlBlockParser.cs index 2609a3996..081b9808c 100644 --- a/src/Markdig/Parsers/HtmlBlockParser.cs +++ b/src/Markdig/Parsers/HtmlBlockParser.cs @@ -275,7 +275,8 @@ private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int Column = startColumn, Type = type, // By default, setup to the end of line - Span = new SourceSpan(startPosition, startPosition + state.Line.End) + Span = new SourceSpan(startPosition, startPosition + state.Line.End), + LinesBefore = state.UseLinesBefore(), }); return BlockState.Continue; } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 6aaa495dc..2220b4db8 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -19,7 +19,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) if (obj is FencedCodeBlock fencedCodeBlock) { renderer.RenderLinesBefore(obj); - var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.FencedCharCount); + var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); renderer.Write(opening); if (fencedCodeBlock.WhitespaceAfterFencedChar != null) @@ -55,7 +55,8 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.WriteLine(); renderer.WriteLeafRawLines(obj, true); - renderer.Write(opening); + var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); + renderer.Write(closing); renderer.RenderLinesAfter(obj); } else diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index 0cc59dc95..4243318ee 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -10,7 +10,9 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { - renderer.WriteLeafRawLines(obj, true, false); + renderer.RenderLinesBefore(obj); + renderer.WriteLeafRawLines(obj, false, false); + renderer.RenderLinesAfter(obj); } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index 1aafeff29..ad6680e0a 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -45,7 +45,12 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// /// Gets or sets the fenced character count used to open this fenced code block. /// - public int FencedCharCount { get; set; } + public int OpeningFencedCharCount { get; set; } + + /// + /// Gets or sets the fenced character count used to open this fenced code block. + /// + public int ClosingFencedCharCount { get; set; } /// /// Gets or sets the fenced character used to open and close this fenced code block. diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index d2cd3068d..729fd1c72 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -42,7 +42,12 @@ public interface IFencedBlock : IBlock /// /// Gets or sets the fenced character count used to open this fenced code block. /// - int FencedCharCount { get; set; } + public int OpeningFencedCharCount { get; set; } + + /// + /// Gets or sets the fenced character count used to open this fenced code block. + /// + public int ClosingFencedCharCount { get; set; } /// /// Gets or sets the fenced character used to open and close this fenced code block. From bfc1152b8a2d248eb7c4580bbb9597aa0bdba444 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 25 Sep 2020 22:35:49 +0200 Subject: [PATCH 014/120] default newline after Html block, fix newlines before list block --- src/Markdig.Tests/TestCst.cs | 3 ++- src/Markdig/Parsers/ListBlockParser.cs | 1 + src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index b54a6c5fe..eb761bd60 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -136,7 +136,7 @@ public void TestNewLinesBeforeAndAfter2() [TestCase("- i1\n- i2")] [TestCase("- i1\n - i2")] [TestCase("- i1\n - i1.1\n - i1.2")] - public void TestUnorederedList(string value) + public void TestUnorderedList(string value) { RoundTrip(value); } @@ -226,6 +226,7 @@ public void TestThematicBreak(string value) [TestCase("\n> q\n\n")] [TestCase("> q\n\np")] [TestCase("p\n\n> q\n\n# h")] + [TestCase(">**b**\n>\n>p\n>\np\n")] public void TestBlockQuote_Paragraph(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 7d8ce930c..e6ae81bb7 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -299,6 +299,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) OrderedDelimiter = listInfo.OrderedDelimiter, DefaultOrderedStart = listInfo.DefaultOrderedStart, OrderedStart = listInfo.OrderedStart, + LinesBefore = state.UseLinesBefore(), }; state.NewBlocks.Push(newList); } diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index 4243318ee..d6a0a0304 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -11,7 +11,7 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { renderer.RenderLinesBefore(obj); - renderer.WriteLeafRawLines(obj, false, false); + renderer.WriteLeafRawLines(obj, true, false); renderer.RenderLinesAfter(obj); } } From 68530aa4e07ac68bc8fb63f51e31e37b2f388a6c Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 26 Sep 2020 12:59:48 +0200 Subject: [PATCH 015/120] improve linebreak handling --- src/Markdig.Tests/TestCst.cs | 23 +++++++++++++++++-- .../Inlines/LineBreakInlineRenderer.cs | 5 ++-- .../Renderers/Normalize/QuoteBlockRenderer.cs | 6 ++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index eb761bd60..04bfbf0be 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -1,3 +1,4 @@ +using Markdig.Renderers; using Markdig.Renderers.Normalize; using Markdig.Syntax; using NUnit.Framework; @@ -7,6 +8,11 @@ /// General notes /// - whitespace can occur before, between and after symbols /// +/// TODO: +/// - \r\n, \r, \n +/// - \t and spaces +/// - html entities i.e. > +/// namespace Markdig.Tests { [TestFixture] @@ -55,9 +61,10 @@ private void RoundTrip(string markdown) MarkdownPipeline pipeline = pipelineBuilder.Build(); MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); + //var nr = new NormalizeRenderer(sw); + var hr = new HtmlRenderer(sw); - nr.Write(markdownDocument); + hr.Write(markdownDocument); Assert.AreEqual(markdown, sw.ToString()); } @@ -136,6 +143,12 @@ public void TestNewLinesBeforeAndAfter2() [TestCase("- i1\n- i2")] [TestCase("- i1\n - i2")] [TestCase("- i1\n - i1.1\n - i1.2")] + [TestCase("- i1 \n- i2 \n")] + [TestCase("- i1 \n- i2 \n")] + [TestCase(" - i1")] + [TestCase(" - i1")] + [TestCase(" - i1")] + [TestCase("\t- i1")] public void TestUnorderedList(string value) { RoundTrip(value); @@ -197,7 +210,13 @@ public void TestUnorderedListItem_BlockQuote(string value) [TestCase("> quote")] [TestCase("> quote")] [TestCase(" > quote")] + [TestCase(">q\n>q")] + [TestCase(">q\n>q\n>q")] [TestCase(">q\n>\n>q")] + [TestCase(">q\n>\n>\n>q")] + [TestCase(">q\n>\n>\n>\n>q")] + [TestCase(">q\n>\n>q\n>\n>q")] + [TestCase(">**q**\n>p\n")] public void TestBlockQuote(string value) { RoundTrip(value); diff --git a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs index ba3f94ed9..59302546a 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs @@ -19,9 +19,10 @@ public class LineBreakInlineRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, LineBreakInline obj) { - if (obj.IsHard) + if (obj.IsHard && obj.IsBackslash) { - renderer.Write(obj.IsBackslash ? "\\" : " "); + renderer.Write("\\"); + //renderer.Write(obj.IsBackslash ? "\\" : " "); } renderer.WriteLine(); } diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 43f341b9e..3fed1104d 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -18,10 +18,10 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) renderer.Write(quoteBlock.BeforeWhitespace); var quoteIndent = quoteBlock.HasSpaceAfterQuoteChar ? quoteBlock.QuoteChar + " " : quoteBlock.QuoteChar.ToString(); - //renderer.PushIndent(quoteIndent); - renderer.Write(quoteIndent); + renderer.PushIndent(quoteIndent); + //renderer.Write(quoteIndent); renderer.WriteChildren(quoteBlock); - //renderer.PopIndent(); + renderer.PopIndent(); renderer.FinishBlock(); renderer.RenderLinesAfter(quoteBlock); From aff7604b4b9ddc63d5476db5fceeca46df2c122a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 26 Sep 2020 13:34:37 +0200 Subject: [PATCH 016/120] fix completely broken unordered list --- src/Markdig.Tests/TestCst.cs | 8 +++++--- src/Markdig/Parsers/ListBlockParser.cs | 2 +- src/Markdig/Parsers/UnorderedListItemParser.cs | 2 +- src/Markdig/Renderers/Normalize/ListRenderer.cs | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 04bfbf0be..307ade9bf 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -61,10 +61,9 @@ private void RoundTrip(string markdown) MarkdownPipeline pipeline = pipelineBuilder.Build(); MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); var sw = new StringWriter(); - //var nr = new NormalizeRenderer(sw); - var hr = new HtmlRenderer(sw); + var nr = new NormalizeRenderer(sw); - hr.Write(markdownDocument); + nr.Write(markdownDocument); Assert.AreEqual(markdown, sw.ToString()); } @@ -165,6 +164,9 @@ public void TestUnorderedList_Paragraph(string value) [TestCase("1. i")] [TestCase("1. i ")] [TestCase("1. i ")] + + [TestCase("1. i1\n2. i2")] + [TestCase("1. i1\n2. i2\n a. i2.1")] public void TestOrderedList(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index e6ae81bb7..4b0e4cf69 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -303,7 +303,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) }; state.NewBlocks.Push(newList); } - + state.NextChar(); // skip space after list marker return BlockState.Continue; } diff --git a/src/Markdig/Parsers/UnorderedListItemParser.cs b/src/Markdig/Parsers/UnorderedListItemParser.cs index 11b718315..4b2d08ef3 100644 --- a/src/Markdig/Parsers/UnorderedListItemParser.cs +++ b/src/Markdig/Parsers/UnorderedListItemParser.cs @@ -22,7 +22,7 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out { result = new ListInfo(state.CurrentChar); state.NextChar(); - state.NextChar(); + //state.NextChar(); return true; } } diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 16c068201..5a23cde4c 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -76,9 +76,9 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) renderer.Write(listItem.BeforeWhitespace); renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); renderer.Write(' '); - renderer.PushIndent(" "); + //renderer.PushIndent(" "); renderer.WriteChildren(listItem); - renderer.PopIndent(); + //renderer.PopIndent(); if (i + 1 < listBlock.Count && listBlock.IsLoose) { //renderer.EnsureLine(); From e2cafc6b3d1a396a70fb7a718ab012ec9d32526f Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 26 Sep 2020 13:46:33 +0200 Subject: [PATCH 017/120] fix newline after listblock --- src/Markdig/Renderers/Normalize/ListRenderer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 5a23cde4c..a4f73cdda 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -89,6 +89,12 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } renderer.CompactParagraph = compact; + // TODO: make reusable method? + if (listBlock.Parent is MarkdownDocument md && md.LastChild != listBlock) + { + renderer.WriteLine(); + } + renderer.RenderLinesAfter(listBlock); } From 6b1c5bc81659bd2095e10312b8e9ce45aabbb84a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 26 Sep 2020 14:07:57 +0200 Subject: [PATCH 018/120] fix thematic break --- src/Markdig.Tests/TestCst.cs | 10 ++++++++++ src/Markdig/Parsers/ListBlockParser.cs | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 307ade9bf..6816fef33 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -139,6 +139,7 @@ public void TestNewLinesBeforeAndAfter2() [TestCase("- i1")] [TestCase("- i1 ")] [TestCase("- i1\n")] + [TestCase("- i1\n\n")] [TestCase("- i1\n- i2")] [TestCase("- i1\n - i2")] [TestCase("- i1\n - i1.1\n - i1.2")] @@ -148,6 +149,8 @@ public void TestNewLinesBeforeAndAfter2() [TestCase(" - i1")] [TestCase(" - i1")] [TestCase("\t- i1")] + [TestCase("- i1\n\n- i1")] + [TestCase("- i1\n\n\n- i1")] public void TestUnorderedList(string value) { RoundTrip(value); @@ -160,6 +163,13 @@ public void TestUnorderedList_Paragraph(string value) RoundTrip(value); } + [TestCase("- i1\n\n---\n")] + [TestCase("- i1\n\n\n---\n")] + public void TestUnorderedList_ThematicBreak(string value) + { + RoundTrip(value); + } + [TestCase("1. i")] [TestCase("1. i")] [TestCase("1. i ")] diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 4b0e4cf69..b51d813c3 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -92,7 +92,9 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) if (result.IsBreak()) { // TODO: We remove the thematic break, as it will be created later, but this is inefficient, try to find another way - processor.NewBlocks.Pop(); + var thematicBreak = processor.NewBlocks.Pop(); + var linesBefore = thematicBreak.LinesBefore; + processor.BeforeLines = linesBefore; return BlockState.None; } } From fa1c1170114f4e125367482c8449827c58837dd6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 26 Sep 2020 14:24:58 +0200 Subject: [PATCH 019/120] experiemnt with list rendering --- src/Markdig/Parsers/ListBlockParser.cs | 3 ++- src/Markdig/Renderers/Normalize/ListRenderer.cs | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index b51d813c3..566f3b3d8 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -268,7 +268,8 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) ColumnWidth = columnWidth, Order = order, BeforeWhitespace = beforeWhitespace, - Span = new SourceSpan(sourcePosition, sourceEndPosition) + Span = new SourceSpan(sourcePosition, sourceEndPosition), + LinesBefore = state.UseLinesBefore(), }; state.NewBlocks.Push(newListItem); diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index a4f73cdda..45b6d0c4c 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -73,17 +73,19 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) var item = listBlock[i]; var listItem = (ListItemBlock) item; //renderer.EnsureLine(); + renderer.RenderLinesBefore(listItem); renderer.Write(listItem.BeforeWhitespace); renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); renderer.Write(' '); //renderer.PushIndent(" "); renderer.WriteChildren(listItem); //renderer.PopIndent(); - if (i + 1 < listBlock.Count && listBlock.IsLoose) - { - //renderer.EnsureLine(); - renderer.WriteLine(); - } + //if (i + 1 < listBlock.Count && listBlock.IsLoose) + //{ + // //renderer.EnsureLine(); + // renderer.WriteLine(); + //} + renderer.RenderLinesAfter(listItem); writeLine = true; } } From 68d12d0212e02dd3764d434aaa66fce5ea8ce501 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 27 Sep 2020 13:16:53 +0200 Subject: [PATCH 020/120] fix newlines and list blocks --- src/Markdig/Parsers/ListBlockParser.cs | 87 ++++++++++--------- .../Renderers/Normalize/ListRenderer.cs | 10 +-- .../Renderers/Normalize/NormalizeRenderer.cs | 2 +- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 566f3b3d8..30435d82f 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -131,7 +131,9 @@ private BlockState TryContinueListItem(BlockProcessor state, ListItemBlock listI if (!(state.NextContinue is ListBlock)) { list.CountAllBlankLines++; - listItem.Add(new BlankLineBlock()); + //listItem.Add(new BlankLineBlock()); + //state.BeforeLines ??= new List(); + //state.BeforeLines.Add(state.Line); } list.CountBlankLinesReset++; } @@ -312,48 +314,49 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) public override bool Close(BlockProcessor processor, Block blockToClose) { - // Process only if we have blank lines - if (blockToClose is ListBlock listBlock && listBlock.CountAllBlankLines > 0) - { - if (listBlock.Parent is ListItemBlock parentListItemBlock && - listBlock.LastChild is ListItemBlock lastListItem && - lastListItem.LastChild is BlankLineBlock) - { - // Inform the outer list that we have a blank line - var parentList = (ListBlock)parentListItemBlock.Parent; - - parentList.CountAllBlankLines++; - parentListItemBlock.Add(new BlankLineBlock()); - } - - for (int listIndex = listBlock.Count - 1; listIndex >= 0; listIndex--) - { - var listItem = (ListItemBlock)listBlock[listIndex]; - - for (int i = listItem.Count - 1; i >= 0; i--) - { - if (listItem[i] is BlankLineBlock) - { - if (i == listItem.Count - 1 ? listIndex < listBlock.Count - 1 : i > 0) - { - listBlock.IsLoose = true; - } - - listItem.RemoveAt(i); - - // If we have removed all blank lines, we can exit - listBlock.CountAllBlankLines--; - if (listBlock.CountAllBlankLines == 0) - { - goto done; - } - } - } - } - } - - done: return true; + // // Process only if we have blank lines + // if (blockToClose is ListBlock listBlock && listBlock.CountAllBlankLines > 0) + // { + // if (listBlock.Parent is ListItemBlock parentListItemBlock && + // listBlock.LastChild is ListItemBlock lastListItem && + // lastListItem.LastChild is BlankLineBlock) + // { + // // Inform the outer list that we have a blank line + // var parentList = (ListBlock)parentListItemBlock.Parent; + + // parentList.CountAllBlankLines++; + // parentListItemBlock.Add(new BlankLineBlock()); + // } + + // for (int listIndex = listBlock.Count - 1; listIndex >= 0; listIndex--) + // { + // var listItem = (ListItemBlock)listBlock[listIndex]; + + // for (int i = listItem.Count - 1; i >= 0; i--) + // { + // if (listItem[i] is BlankLineBlock) + // { + // if (i == listItem.Count - 1 ? listIndex < listBlock.Count - 1 : i > 0) + // { + // listBlock.IsLoose = true; + // } + + // //listItem.RemoveAt(i); + + // // If we have removed all blank lines, we can exit + // //listBlock.CountAllBlankLines--; + // //if (listBlock.CountAllBlankLines == 0) + // //{ + // // goto done; + // //} + // } + // } + // } + // } + + //done: + // return true; } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 45b6d0c4c..a2ac29749 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -91,11 +91,11 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } renderer.CompactParagraph = compact; - // TODO: make reusable method? - if (listBlock.Parent is MarkdownDocument md && md.LastChild != listBlock) - { - renderer.WriteLine(); - } + //// TODO: make reusable method? + //if (listBlock.Parent is MarkdownDocument md && md.LastChild != listBlock) + //{ + // renderer.WriteLine(); + //} renderer.RenderLinesAfter(listBlock); } diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index b83a4fa9e..d5e40d07d 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -54,7 +54,7 @@ public void FinishBlock(bool derp = false) { if (!IsLastInContainer) { - WriteLine(); + WriteLine(); // TODO: remove this method? } } From 4c2b46e0fc2d6a75408e16eb3fdc3435d83f25d4 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 27 Sep 2020 13:27:14 +0200 Subject: [PATCH 021/120] fix listblock-paragraph --- src/Markdig.Tests/TestCst.cs | 2 ++ src/Markdig/Renderers/Normalize/ListRenderer.cs | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 6816fef33..59cb5493b 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -158,6 +158,8 @@ public void TestUnorderedList(string value) [TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline [TestCase("- i1\n\n\np\n")] + [TestCase("- i1\n\np")] + [TestCase("- i1\n\np\n")] public void TestUnorderedList_Paragraph(string value) { RoundTrip(value); diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index a2ac29749..45b6d0c4c 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -91,11 +91,11 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } renderer.CompactParagraph = compact; - //// TODO: make reusable method? - //if (listBlock.Parent is MarkdownDocument md && md.LastChild != listBlock) - //{ - // renderer.WriteLine(); - //} + // TODO: make reusable method? + if (listBlock.Parent is MarkdownDocument md && md.LastChild != listBlock) + { + renderer.WriteLine(); + } renderer.RenderLinesAfter(listBlock); } From dbdd752b73fc92247e7b6b6ae4217885a9658e6d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 3 Oct 2020 16:11:01 +0200 Subject: [PATCH 022/120] Keep line info when parsing quoteblocks and render accordingly --- src/Markdig.Tests/TestCst.cs | 179 +++++++++++++++++- src/Markdig/Parsers/QuoteBlockParser.cs | 61 ++++-- src/Markdig/Parsers/ThematicBreakParser.cs | 1 - .../Renderers/Normalize/HtmlBlockRenderer.cs | 3 +- .../Renderers/Normalize/NormalizeRenderer.cs | 10 + .../Renderers/Normalize/QuoteBlockRenderer.cs | 15 +- .../Normalize/ThematicBreakRenderer.cs | 3 +- src/Markdig/Renderers/TextRendererBase.cs | 42 +++- src/Markdig/Syntax/QuoteBlock.cs | 18 +- 9 files changed, 291 insertions(+), 41 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 59cb5493b..2ec15da07 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -151,6 +151,8 @@ public void TestNewLinesBeforeAndAfter2() [TestCase("\t- i1")] [TestCase("- i1\n\n- i1")] [TestCase("- i1\n\n\n- i1")] + + [TestCase("-\ti1")] public void TestUnorderedList(string value) { RoundTrip(value); @@ -220,30 +222,188 @@ public void TestUnorderedListItem_BlockQuote(string value) RoundTrip(value); } - [TestCase(">quote")] - [TestCase("> quote")] - [TestCase("> quote")] - [TestCase(" > quote")] + [TestCase(">q")] + [TestCase(" >q")] + [TestCase(" >q")] + [TestCase(" >q")] + [TestCase("> q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase("> q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(">q\n>q")] + [TestCase(">q\n >q")] + [TestCase(">q\n >q")] + [TestCase(">q\n >q")] + [TestCase(">q\n> q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n> q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + + [TestCase(" >q\n>q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + + [TestCase(" >q\n>q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + + [TestCase("> q\n>q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase("> q\n>q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(">q\n>q\n>q")] [TestCase(">q\n>\n>q")] + [TestCase(">q\np\n>q")] [TestCase(">q\n>\n>\n>q")] [TestCase(">q\n>\n>\n>\n>q")] [TestCase(">q\n>\n>q\n>\n>q")] - [TestCase(">**q**\n>p\n")] + [TestCase("> **q**\n>p\n")] + [TestCase("p\n\n> **q**\n>p\n")] + + [TestCase("> q\np\n> q")] // lazy + [TestCase("> q\n> q\np")] // lazy public void TestBlockQuote(string value) { RoundTrip(value); } - //[TestCase("---")] + [TestCase("---")] [TestCase(" ---")] [TestCase(" ---")] [TestCase(" ---")] - //[TestCase("--- ")] + [TestCase("--- ")] [TestCase(" --- ")] [TestCase(" --- ")] [TestCase(" --- ")] + [TestCase("---\n")] + [TestCase("---\n\n")] [TestCase("---\np")] [TestCase("---\n\np")] [TestCase("---\n# h")] @@ -272,6 +432,7 @@ public void TestBlockQuote_Header(string value) } [TestCase(">- i1\n>- i2\n")] + [TestCase("> **p** p\n>- i1\n>- i2\n")] public void TestBlockQuote_ListBlock(string value) { RoundTrip(value); @@ -498,6 +659,10 @@ public void TestCodeBlock(string value) RoundTrip(value); } + [TestCase("
")] + [TestCase("
\n")] + [TestCase("
\n\n")] + [TestCase("
\n\n# h")] [TestCase("p\n\n
\n")] [TestCase("
\n\n# h")] public void TestHtml(string value) diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 066cfe95d..143006cad 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -4,6 +4,7 @@ using Markdig.Helpers; using Markdig.Syntax; +using System.Diagnostics; namespace Markdig.Parsers { @@ -28,32 +29,35 @@ public override BlockState TryOpen(BlockProcessor processor) return BlockState.None; } - var column = processor.Column; var sourcePosition = processor.Start; // 5.1 Block quotes // A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space. var quoteChar = processor.CurrentChar; + var column = processor.Column; var c = processor.NextChar(); - bool hasSpaceAfterQuoteChar = false; - int whitespaceToAdd = 1; - if (c.IsSpaceOrTab()) - { - processor.NextColumn(); - hasSpaceAfterQuoteChar = true; - whitespaceToAdd += 1; - } + //if (c.IsSpaceOrTab()) + //{ + // processor.NextColumn(); + // hasSpaceAfterQuoteChar = true; + // whitespaceToAdd += 1; + //} //beforeWhitespace.End -= 1 + (hasSpaceAfterQuoteChar ? 1 : 0); - processor.NewBlocks.Push(new QuoteBlock(this) + + var quoteBlock = new QuoteBlock(this) { QuoteChar = quoteChar, Column = column, Span = new SourceSpan(sourcePosition, processor.Line.End), - BeforeWhitespace = processor.PopBeforeWhitespace(column), - HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, LinesBefore = processor.UseLinesBefore() + }; + quoteBlock.QuoteLines.Add(new QuoteBlock.QuoteLine + { + BeforeWhitespace = processor.PopBeforeWhitespace(column), + QuoteChar = true }); - processor.WhitespaceStart += whitespaceToAdd; + processor.NewBlocks.Push(quoteBlock); + processor.WhitespaceStart += 1; return BlockState.Continue; } @@ -71,15 +75,34 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var c = processor.CurrentChar; if (c != quote.QuoteChar) { - return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None; + if (processor.IsBlankLine) + { + return BlockState.BreakDiscard; + } + else + { + var ql = new QuoteBlock.QuoteLine + { + QuoteChar = false, + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Column) + }; + quote.QuoteLines.Add(ql); + return BlockState.None; + } } - - c = processor.NextChar(); // Skip opening char - if (c.IsSpace()) + var quoteLine = new QuoteBlock.QuoteLine { - processor.NextChar(); // Skip following space - } + QuoteChar = true, + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Column) + }; + quote.QuoteLines.Add(quoteLine); + processor.NextChar(); // Skip opening char + //if (c.IsSpace()) + //{ + // processor.NextChar(); // Skip following space + //} + processor.WhitespaceStart = processor.Column; block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; } diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index 8481bd20c..ccb6d67cb 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -34,7 +34,6 @@ public override BlockState TryOpen(BlockProcessor processor) } var startPosition = processor.Start; - var line = processor.Line; // 4.1 Thematic breaks diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index d6a0a0304..a34d72449 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -11,7 +11,8 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { renderer.RenderLinesBefore(obj); - renderer.WriteLeafRawLines(obj, true, false); + renderer.WriteLeafRawLines(obj, false, false); + renderer.RenderLineAfterIfNeeded(obj); renderer.RenderLinesAfter(obj); } } diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index d5e40d07d..c80e62391 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -176,5 +176,15 @@ public void RenderLinesAfter(Block block) } } } + + public void RenderLineAfterIfNeeded(Block block) + { + bool isLast = Equals(block.Parent.LastChild, block); + bool hasLinesAfter = block.LinesAfter != null && block.LinesAfter.Count > 0; + if (!isLast && !hasLinesAfter) + { + WriteLine(); + } + } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 3fed1104d..d026eda2e 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; +using System.Collections.Generic; namespace Markdig.Renderers.Normalize { @@ -17,10 +18,18 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) renderer.RenderLinesBefore(quoteBlock); renderer.Write(quoteBlock.BeforeWhitespace); - var quoteIndent = quoteBlock.HasSpaceAfterQuoteChar ? quoteBlock.QuoteChar + " " : quoteBlock.QuoteChar.ToString(); - renderer.PushIndent(quoteIndent); + //var quoteIndent = quoteBlock.HasSpaceAfterQuoteChar ? quoteBlock.QuoteChar + " " : quoteBlock.QuoteChar.ToString(); + //var quoteIndent = quoteBlock.QuoteChar.ToString(); + + var indents = new List(); + foreach (var quoteLine in quoteBlock.QuoteLines) + { + indents.Add(quoteLine.BeforeWhitespace.ToString() + (quoteLine.QuoteChar ? ">" : "")); + } + + renderer.PushIndent(indents); //renderer.Write(quoteIndent); - renderer.WriteChildren(quoteBlock); + renderer.WriteChildren(quoteBlock); renderer.PopIndent(); renderer.FinishBlock(); diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 7726aff43..4dc1e622b 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -18,7 +18,8 @@ protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj // for now, render always a newline // TODO: only render a newline when not last line - renderer.WriteLine(new string(obj.ThematicChar, obj.ThematicCharCount)); + renderer.Write(new string(obj.ThematicChar, obj.ThematicCharCount)); + renderer.RenderLineAfterIfNeeded(obj); renderer.RenderLinesAfter(obj); } } diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index c93a10388..cb585f936 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -70,11 +70,37 @@ public override object Render(MarkdownObject markdownObject) /// public abstract class TextRendererBase : TextRendererBase where T : TextRendererBase { + private class Indent + { + private readonly string _constant; + private readonly Queue _lineSpecific; + + public Indent(string constant) + { + _constant = constant; + } + public Indent(IEnumerable lineSpecific) + { + _lineSpecific = new Queue(lineSpecific); + } + + public string Next() + { + if (_constant != null) + { + return _constant; + } + if (_lineSpecific.Count == 0) throw new Exception("Indents empty"); + var next = _lineSpecific.Dequeue(); + return next; + } + } + private bool previousWasLine; #if !NETCORE private char[] buffer; #endif - private readonly List indents; + private readonly List indents; /// /// Initializes a new instance of the class. @@ -87,7 +113,7 @@ protected TextRendererBase(TextWriter writer) : base(writer) #endif // We assume that we are starting as if we had previously a newline previousWasLine = true; - indents = new List(); + indents = new List(); } internal void Reset() @@ -121,7 +147,13 @@ public T EnsureLine() public void PushIndent(string indent) { if (indent == null) ThrowHelper.ArgumentNullException(nameof(indent)); - indents.Add(indent); + indents.Add(new Indent(indent)); + } + + public void PushIndent(IEnumerable lineSpecific) + { + if (indents == null) ThrowHelper.ArgumentNullException(nameof(indents)); + indents.Add(new Indent(lineSpecific)); } public void PopIndent() @@ -137,7 +169,9 @@ private void WriteIndent() previousWasLine = false; for (int i = 0; i < indents.Count; i++) { - Writer.Write(indents[i]); + var indent = indents[i]; + var indentText = indent.Next(); + Writer.Write(indentText); } } } diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index 3ab29e2bc..a185a9e1a 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; +using System.Collections.Generic; namespace Markdig.Syntax { @@ -12,6 +14,15 @@ namespace Markdig.Syntax /// public class QuoteBlock : ContainerBlock { + // ws? >? ws? block|inline + public class QuoteLine + { + public StringSlice BeforeWhitespace { get; set; } + + // support lazy lines + public bool QuoteChar { get; set; } + } + /// /// Initializes a new instance of the class. /// @@ -20,14 +31,11 @@ public QuoteBlock(BlockParser parser) : base(parser) { } + public List QuoteLines { get; set; } = new List(); + /// /// Gets or sets the quote character (usually `>`) /// public char QuoteChar { get; set; } - - /// - /// True if a space is parsed between the > character and the paragraph - /// - public bool HasSpaceAfterQuoteChar { get; internal set; } } } \ No newline at end of file From d7f6a94f12a83cc1cd6f6e2dcacea879bc678558 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 3 Oct 2020 18:54:45 +0200 Subject: [PATCH 023/120] cleanup, add some testcases --- src/Markdig/Helpers/StringSlice.cs | 7 ------- src/Markdig/Renderers/Normalize/ParagraphRenderer.cs | 10 +++++----- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 84c19d2ca..786046d01 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -26,7 +26,6 @@ public StringSlice(string text) { Text = text; Start = 0; - ActualStart = 0; End = (Text?.Length ?? 0) - 1; } @@ -44,7 +43,6 @@ public StringSlice(string text, int start, int end) Text = text; Start = start; - ActualStart = start; End = end; } @@ -58,11 +56,6 @@ public StringSlice(string text, int start, int end) /// public int Start { readonly get; set; } - /// - /// Gets or sets the start position within . - /// - public int ActualStart { get; } - /// /// Gets or sets the end position (inclusive) within . /// diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 00ead23c2..408975c53 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -14,13 +14,13 @@ namespace Markdig.Renderers.Normalize [DebuggerDisplay("renderer.Writer.ToString()")] public class ParagraphRenderer : NormalizeObjectRenderer { - protected override void Write(NormalizeRenderer renderer, ParagraphBlock parapgraph) + protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragraph) { - renderer.RenderLinesBefore(parapgraph); - renderer.Write(parapgraph.BeforeWhitespace); - renderer.WriteLeafInline(parapgraph); + renderer.RenderLinesBefore(paragraph); + renderer.Write(paragraph.BeforeWhitespace); + renderer.WriteLeafInline(paragraph); renderer.FinishBlock(); - renderer.RenderLinesAfter(parapgraph); + renderer.RenderLinesAfter(paragraph); } } } \ No newline at end of file From d31fa47cd1726d27b0c81048b834bbd1530249f6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 3 Oct 2020 18:55:02 +0200 Subject: [PATCH 024/120] add some testcases --- src/Markdig.Tests/TestCst.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 2ec15da07..87c1a06d4 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -151,6 +151,7 @@ public void TestNewLinesBeforeAndAfter2() [TestCase("\t- i1")] [TestCase("- i1\n\n- i1")] [TestCase("- i1\n\n\n- i1")] + [TestCase("- i1\n - i1.1\n - i1.1.1\n")] [TestCase("-\ti1")] public void TestUnorderedList(string value) @@ -174,6 +175,12 @@ public void TestUnorderedList_ThematicBreak(string value) RoundTrip(value); } + [TestCase("- i1\n - i1.1\n ```\n code\n ```")] + public void TestUnorderedList_CodeBlock(string value) + { + RoundTrip(value); + } + [TestCase("1. i")] [TestCase("1. i")] [TestCase("1. i ")] @@ -419,6 +426,9 @@ public void TestThematicBreak(string value) [TestCase("\n> q\n\n")] [TestCase("> q\n\np")] [TestCase("p\n\n> q\n\n# h")] + + //https://github.com/lunet-io/markdig/issues/480 + [TestCase(">\np")] [TestCase(">**b**\n>\n>p\n>\np\n")] public void TestBlockQuote_Paragraph(string value) { @@ -643,7 +653,14 @@ public void TestAutolinkInline(string value) RoundTrip(value); } + [TestCase("```c```")] + + [TestCase(" l")] + [TestCase("\tl")] + [TestCase("\tl1\n l1")] + [TestCase("```\nc\n```")] + [TestCase("```\nc\n```\n")] [TestCase("\n```\nc\n```")] [TestCase("\n\n```\nc\n```")] [TestCase("```\nc\n```\n")] @@ -654,6 +671,7 @@ public void TestAutolinkInline(string value) [TestCase("\n\n```\nc\n```\n\n")] [TestCase("```\nc\n````")] + [TestCase("p\n\n```\nc\n```")] public void TestCodeBlock(string value) { RoundTrip(value); @@ -669,5 +687,12 @@ public void TestHtml(string value) { RoundTrip(value); } + + [TestCase("h1===\n")] + [TestCase("h2---\n")] + public void TestSetextHeading(string value) + { + RoundTrip(value); + } } } From ac6ceb23e8ae326a7749ac851a0a13cde4b6242f Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 3 Oct 2020 23:13:04 +0200 Subject: [PATCH 025/120] restore whitespace parsing in BlockProcessor --- src/Markdig.Tests/TestCst.cs | 14 +++++++-- src/Markdig/Parsers/BlockProcessor.cs | 29 ++++++++++++------- src/Markdig/Parsers/ListBlockParser.cs | 9 +++--- src/Markdig/Parsers/ParagraphBlockParser.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 26 +++++++---------- src/Markdig/Parsers/ThematicBreakParser.cs | 4 +++ .../Renderers/Normalize/ListRenderer.cs | 13 +++------ .../Renderers/Normalize/QuoteBlockRenderer.cs | 1 + .../Normalize/ThematicBreakRenderer.cs | 5 +++- src/Markdig/Syntax/ThematicBreakBlock.cs | 3 ++ 10 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs index 87c1a06d4..435866782 100644 --- a/src/Markdig.Tests/TestCst.cs +++ b/src/Markdig.Tests/TestCst.cs @@ -148,12 +148,13 @@ public void TestNewLinesBeforeAndAfter2() [TestCase(" - i1")] [TestCase(" - i1")] [TestCase(" - i1")] - [TestCase("\t- i1")] [TestCase("- i1\n\n- i1")] [TestCase("- i1\n\n\n- i1")] [TestCase("- i1\n - i1.1\n - i1.1.1\n")] [TestCase("-\ti1")] + [TestCase("-\ti1\n-\ti2")] + [TestCase("-\ti1\n- i2\n-\ti3")] public void TestUnorderedList(string value) { RoundTrip(value); @@ -396,6 +397,9 @@ public void TestUnorderedListItem_BlockQuote(string value) [TestCase("> q\np\n> q")] // lazy [TestCase("> q\n> q\np")] // lazy + + [TestCase(">>q")] + [TestCase(" > > q")] public void TestBlockQuote(string value) { RoundTrip(value); @@ -409,6 +413,10 @@ public void TestBlockQuote(string value) [TestCase(" --- ")] [TestCase(" --- ")] [TestCase(" --- ")] + [TestCase("- - -")] + [TestCase(" - - -")] + [TestCase(" - - - ")] + [TestCase("-- -")] [TestCase("---\n")] [TestCase("---\n\n")] [TestCase("---\np")] @@ -428,8 +436,8 @@ public void TestThematicBreak(string value) [TestCase("p\n\n> q\n\n# h")] //https://github.com/lunet-io/markdig/issues/480 - [TestCase(">\np")] - [TestCase(">**b**\n>\n>p\n>\np\n")] + //[TestCase(">\np")] + //[TestCase(">**b**\n>\n>p\n>\np\n")] public void TestBlockQuote_Paragraph(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index ebeae0967..8fc9c1bb0 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -178,11 +178,18 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo public int WhitespaceStart { get; set; } - public StringSlice PopBeforeWhitespace(int column) + // NOTE: Line.Text is full source, not the line (!) + public StringSlice PopBeforeWhitespace(int end) { - var stringSlice = new StringSlice(Line.Text, WhitespaceStart, column - 1); - //var stringSlice = new StringSlice(Line.Text, Line.Start + WhitespaceStart, Line.Start + column - 1); - WhitespaceStart = column; + var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); + WhitespaceStart = end + 1; + return stringSlice; + } + + public StringSlice PopBeforeWhitespace(int start, int end) + { + var stringSlice = new StringSlice(Line.Text, start, end); + WhitespaceStart = end + 1; return stringSlice; } @@ -270,19 +277,19 @@ public void ParseIndent() var startBeforeIndent = Start; var previousColumnBeforeIndent = ColumnBeforeIndent; var columnBeforeIndent = Column; - while (c !='\0') + while (c != '\0') { if (c == '\t') { Column = CharHelper.AddTab(Column); } - //else if (c == ' ') - //{ - // Column++; - //} + else if (c == ' ') + { + Column++; + } else { - break; + break; } c = Line.NextChar(); } @@ -898,7 +905,7 @@ private void ResetLine(StringSlice newLine) ColumnBeforeIndent = 0; StartBeforeIndent = Start; originalLineStart = newLine.Start; - WhitespaceStart = 0; + WhitespaceStart = newLine.Start; } private void Reset() diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 30435d82f..b3cf892d6 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -207,6 +207,9 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) state.GoToColumn(initColumn); return BlockState.None; } + var bulletLength = 1; // TODO: fix for ordered + var whitespaceBefore = state.PopBeforeWhitespace(state.CurrentLineStartPosition, state.Start - 1 - bulletLength); // -1: + int whitespaceAfterStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; @@ -215,7 +218,6 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) // Item starting with a blank line int columnWidth; - var beforeWhitespace = state.PopBeforeWhitespace(initColumn); // Do we have a blank line right after the bullet? if (c == '\0') @@ -262,14 +264,14 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) } } - state.WhitespaceStart += 2; int.TryParse(listInfo.OrderedStart, out int order); var newListItem = new ListItemBlock(this) { Column = initColumn, ColumnWidth = columnWidth, Order = order, - BeforeWhitespace = beforeWhitespace, + BeforeWhitespace = whitespaceBefore, + AfterWhitespace = state.PopBeforeWhitespace(whitespaceAfterStart, state.Start - 1), Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), }; @@ -308,7 +310,6 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) }; state.NewBlocks.Push(newList); } - state.NextChar(); // skip space after list marker return BlockState.Continue; } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index bc12da7a8..713c0a75c 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -28,7 +28,7 @@ public override BlockState TryOpen(BlockProcessor processor) { Column = processor.Column, Span = new SourceSpan(processor.Line.Start, processor.Line.End), - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Column), + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), LinesBefore = processor.UseLinesBefore() }); return BlockState.Continue; diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 143006cad..230e3fe6d 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -53,11 +53,11 @@ public override BlockState TryOpen(BlockProcessor processor) }; quoteBlock.QuoteLines.Add(new QuoteBlock.QuoteLine { - BeforeWhitespace = processor.PopBeforeWhitespace(column), + BeforeWhitespace = processor.PopBeforeWhitespace(processor.ColumnBeforeIndent, sourcePosition - 1), QuoteChar = true }); processor.NewBlocks.Push(quoteBlock); - processor.WhitespaceStart += 1; + processor.WhitespaceStart = sourcePosition + 1; return BlockState.Continue; } @@ -81,28 +81,22 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) } else { - var ql = new QuoteBlock.QuoteLine + quote.QuoteLines.Add(new QuoteBlock.QuoteLine { QuoteChar = false, - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Column) - }; - quote.QuoteLines.Add(ql); + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + }); return BlockState.None; } } - var quoteLine = new QuoteBlock.QuoteLine + quote.QuoteLines.Add(new QuoteBlock.QuoteLine { QuoteChar = true, - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Column) - }; - quote.QuoteLines.Add(quoteLine); - processor.NextChar(); // Skip opening char - //if (c.IsSpace()) - //{ - // processor.NextChar(); // Skip following space - //} + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + }); - processor.WhitespaceStart = processor.Column; + processor.NextChar(); // Skip quote marker char + processor.WhitespaceStart = processor.Start; block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; } diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index ccb6d67cb..e170b96c0 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -91,7 +91,11 @@ public override BlockState TryOpen(BlockProcessor processor) Span = new SourceSpan(startPosition, line.End), ThematicChar = breakChar, ThematicCharCount = breakCharCount, + // TODO: should we separate whitespace before/after? + //BeforeWhitespace = beforeWhitespace, + //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), LinesBefore = processor.UseLinesBefore(), + Content = new StringSlice(line.Text, processor.CurrentLineStartPosition, line.End) }); return BlockState.BreakDiscard; } diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 45b6d0c4c..4602529ac 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -72,19 +72,14 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } var item = listBlock[i]; var listItem = (ListItemBlock) item; - //renderer.EnsureLine(); renderer.RenderLinesBefore(listItem); renderer.Write(listItem.BeforeWhitespace); renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); - renderer.Write(' '); - //renderer.PushIndent(" "); + renderer.Write(listItem.AfterWhitespace); + + renderer.WriteChildren(listItem); - //renderer.PopIndent(); - //if (i + 1 < listBlock.Count && listBlock.IsLoose) - //{ - // //renderer.EnsureLine(); - // renderer.WriteLine(); - //} + renderer.RenderLinesAfter(listItem); writeLine = true; } diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index d026eda2e..85d2ed4ad 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -28,6 +28,7 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) } renderer.PushIndent(indents); + // indents are not always written, depending on the childs //renderer.Write(quoteIndent); renderer.WriteChildren(quoteBlock); renderer.PopIndent(); diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 4dc1e622b..6f5e8001b 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Syntax; namespace Markdig.Renderers.Normalize @@ -18,7 +19,9 @@ protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj // for now, render always a newline // TODO: only render a newline when not last line - renderer.Write(new string(obj.ThematicChar, obj.ThematicCharCount)); + //renderer.Write(obj.BeforeWhitespace); + renderer.Write(obj.Content); + //renderer.Write(obj.AfterWhitespace); renderer.RenderLineAfterIfNeeded(obj); renderer.RenderLinesAfter(obj); } diff --git a/src/Markdig/Syntax/ThematicBreakBlock.cs b/src/Markdig/Syntax/ThematicBreakBlock.cs index 2de9ed112..f74842553 100644 --- a/src/Markdig/Syntax/ThematicBreakBlock.cs +++ b/src/Markdig/Syntax/ThematicBreakBlock.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; namespace Markdig.Syntax @@ -22,5 +23,7 @@ public ThematicBreakBlock(BlockParser parser) : base(parser) public char ThematicChar { get; set; } public int ThematicCharCount { get; set; } + + public StringSlice Content; } } \ No newline at end of file From 111d3d3362b0e4d26178c873fbb1f64a28bc85ac Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 4 Oct 2020 21:20:32 +0200 Subject: [PATCH 026/120] fix multiline quote blocks, split tests into file per node type --- .../Inlines/TestAutoLinkInline.cs | 33 + .../RoundtripSpecs/Inlines/TestCodeInline.cs | 52 ++ .../RoundtripSpecs/Inlines/TestImage.cs | 25 + .../RoundtripSpecs/Inlines/TestLinkInline.cs | 117 +++ .../RoundtripSpecs/TestAtxHeading.cs | 32 + .../RoundtripSpecs/TestCodeBlock.cs | 20 + .../RoundtripSpecs/TestFencedCodeBlock.cs | 27 + .../RoundtripSpecs/TestHelper.cs | 23 + .../RoundtripSpecs/TestHtmlBlock.cs | 20 + .../RoundtripSpecs/TestOrderedList.cs | 21 + .../RoundtripSpecs/TestParagraph.cs | 65 ++ .../RoundtripSpecs/TestQuoteBlock.cs | 218 ++++++ .../RoundtripSpecs/TestSetextHeading.cs | 16 + .../RoundtripSpecs/TestThematicBreak.cs | 33 + .../RoundtripSpecs/TestUnorderedList.cs | 78 ++ src/Markdig.Tests/TestCst.cs | 706 ------------------ src/Markdig/Parsers/BlockProcessor.cs | 11 +- src/Markdig/Parsers/HeadingBlockParser.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 2 +- .../Renderers/Normalize/ParagraphRenderer.cs | 3 +- .../Renderers/Normalize/QuoteBlockRenderer.cs | 2 +- src/Markdig/Renderers/TextRendererBase.cs | 6 +- src/Markdig/Syntax/LeafBlock.cs | 1 + src/Markdig/Syntax/QuoteBlock.cs | 2 + 24 files changed, 800 insertions(+), 715 deletions(-) create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestHelper.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs delete mode 100644 src/Markdig.Tests/TestCst.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs new file mode 100644 index 000000000..a38485e38 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestAutoLinkInline + { + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + + [TestCase("< http://a>")] + [TestCase(" < http://a>")] + [TestCase("< http://a> ")] + [TestCase(" < http://a> ")] + + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + + [TestCase("< http://a >")] + [TestCase(" < http://a >")] + [TestCase("< http://a > ")] + [TestCase(" < http://a > ")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs new file mode 100644 index 000000000..7fe63124a --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestCodeInline + { + [TestCase("``")] + [TestCase(" ``")] + [TestCase("`` ")] + [TestCase(" `` ")] + + [TestCase("`a`")] + [TestCase(" `a`")] + [TestCase("`a` ")] + [TestCase(" `a` ")] + + [TestCase("` a`")] + [TestCase(" ` a`")] + [TestCase("` a` ")] + [TestCase(" ` a` ")] + + [TestCase("`a `")] + [TestCase(" `a `")] + [TestCase("`a ` ")] + [TestCase(" `a ` ")] + + [TestCase("``a``")] + [TestCase("- ```a```")] + [TestCase("p ```a``` p")] + + [TestCase("```c```")] + + // broken + //[TestCase("```a```")] + [TestCase("```a``` p")] + [TestCase("```a`b`c```")] + //[TestCase("p\n\n```a``` p")] + //[TestCase("```a``` p\n```a``` p")] + + /// : intentionally trimmed. TODO: decide on how to handle + //[TestCase("` a `")] + //[TestCase(" ` a `")] + //[TestCase("` a ` ")] + //[TestCase(" ` a ` ")] + public void Test_CodeInline(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs new file mode 100644 index 000000000..cee2aea9d --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestImage + { + [TestCase("![](a)")] + [TestCase(" ![](a)")] + [TestCase("![](a) ")] + [TestCase(" ![](a) ")] + [TestCase(" ![description](http://example.com)")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("paragraph ![description](http://example.com)")] + public void TestParagraph(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs new file mode 100644 index 000000000..89e8350f0 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -0,0 +1,117 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestLinkInline + { + [TestCase("[a]")] // TODO: this is not a link but a paragraph + [TestCase("[a]()")] + + [TestCase("[](b)")] + [TestCase(" [](b)")] + [TestCase("[](b) ")] + [TestCase(" [](b) ")] + + [TestCase("[a](b)")] + [TestCase(" [a](b)")] + [TestCase("[a](b) ")] + [TestCase(" [a](b) ")] + + [TestCase("[ a](b)")] + [TestCase(" [ a](b)")] + [TestCase("[ a](b) ")] + [TestCase(" [ a](b) ")] + + [TestCase("[a ](b)")] + [TestCase(" [a ](b)")] + [TestCase("[a ](b) ")] + [TestCase(" [a ](b) ")] + + [TestCase("[ a ](b)")] + [TestCase(" [ a ](b)")] + [TestCase("[ a ](b) ")] + [TestCase(" [ a ](b) ")] + + // below cases are required for a full CST but not have low prio for impl + //[TestCase("[]( b)")] + //[TestCase(" []( b)")] + //[TestCase("[]( b) ")] + //[TestCase(" []( b) ")] + + //[TestCase("[a]( b)")] + //[TestCase(" [a]( b)")] + //[TestCase("[a]( b) ")] + //[TestCase(" [a]( b) ")] + + //[TestCase("[ a]( b)")] + //[TestCase(" [ a]( b)")] + //[TestCase("[ a]( b) ")] + //[TestCase(" [ a]( b) ")] + + //[TestCase("[a ]( b)")] + //[TestCase(" [a ]( b)")] + //[TestCase("[a ]( b) ")] + //[TestCase(" [a ]( b) ")] + + //[TestCase("[ a ]( b)")] + //[TestCase(" [ a ]( b)")] + //[TestCase("[ a ]( b) ")] + //[TestCase(" [ a ]( b) ")] + + //[TestCase("[](b )")] + //[TestCase(" [](b )")] + //[TestCase("[](b ) ")] + //[TestCase(" [](b ) ")] + + //[TestCase("[a](b )")] + //[TestCase(" [a](b )")] + //[TestCase("[a](b ) ")] + //[TestCase(" [a](b ) ")] + + //[TestCase("[ a](b )")] + //[TestCase(" [ a](b )")] + //[TestCase("[ a](b ) ")] + //[TestCase(" [ a](b ) ")] + + //[TestCase("[a ](b )")] + //[TestCase(" [a ](b )")] + //[TestCase("[a ](b ) ")] + //[TestCase(" [a ](b ) ")] + + //[TestCase("[ a ](b )")] + //[TestCase(" [ a ](b )")] + //[TestCase("[ a ](b ) ")] + //[TestCase(" [ a ](b ) ")] + + //[TestCase("[]( b )")] + //[TestCase(" []( b )")] + //[TestCase("[]( b ) ")] + //[TestCase(" []( b ) ")] + + //[TestCase("[a]( b )")] + //[TestCase(" [a]( b )")] + //[TestCase("[a]( b ) ")] + //[TestCase(" [a]( b ) ")] + + //[TestCase("[ a]( b )")] + //[TestCase(" [ a]( b )")] + //[TestCase("[ a]( b ) ")] + //[TestCase(" [ a]( b ) ")] + + //[TestCase("[a ]( b )")] + //[TestCase(" [a ]( b )")] + //[TestCase("[a ]( b ) ")] + //[TestCase(" [a ]( b ) ")] + + //[TestCase("[ a ]( b )")] + //[TestCase(" [ a ]( b )")] + //[TestCase("[ a ]( b ) ")] + //[TestCase(" [ a ]( b ) ")] + public void Test_InlineLink(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs new file mode 100644 index 000000000..b55c3b099 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestAtxHeading + { + [TestCase("# h")] + [TestCase("# h ")] + [TestCase("# h\n#h")] + [TestCase("# h\n #h")] + [TestCase("# h\n # h")] + [TestCase("# h\n # h ")] + [TestCase(" # h \n # h ")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("\n# h\n\np")] + [TestCase("\n# h\n\np\n")] + [TestCase("\n# h\n\np\n\n")] + [TestCase("\n\n# h\n\np\n\n")] + [TestCase("\n\n# h\np\n\n")] + [TestCase("\n\n# h\np\n\n")] + public void TestParagrph(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs new file mode 100644 index 000000000..e0078c49a --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs @@ -0,0 +1,20 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestCodeBlock + { + // A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content. + // l = line + [TestCase(" l")] + [TestCase(" l")] + [TestCase("\tl")] + [TestCase("\tl1\n l1")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs new file mode 100644 index 000000000..87428c7f6 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -0,0 +1,27 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestFencedCodeBlock + { + [TestCase("```\nc\n```")] + [TestCase("```\nc\n```\n")] + [TestCase("\n```\nc\n```")] + [TestCase("\n\n```\nc\n```")] + [TestCase("```\nc\n```\n")] + [TestCase("```\nc\n```\n\n")] + [TestCase("\n```\nc\n```\n")] + [TestCase("\n```\nc\n```\n\n")] + [TestCase("\n\n```\nc\n```\n")] + [TestCase("\n\n```\nc\n```\n\n")] + + [TestCase("```\nc\n````")] + [TestCase("p\n\n```\nc\n```")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs b/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs new file mode 100644 index 000000000..94aaf880b --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs @@ -0,0 +1,23 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using NUnit.Framework; +using System.IO; + +namespace Markdig.Tests.RoundtripSpecs +{ + internal static class TestHelper + { + internal static void RoundTrip(string markdown) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs new file mode 100644 index 000000000..ee9a36c21 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs @@ -0,0 +1,20 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestHtmlBlock + { + [TestCase("
")] + [TestCase("
\n")] + [TestCase("
\n\n")] + [TestCase("
\n\n# h")] + [TestCase("p\n\n
\n")] + [TestCase("
\n\n# h")] + public void TestHtml(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs new file mode 100644 index 000000000..32f5eb80d --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -0,0 +1,21 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestOrderedList + { + [TestCase("1. i")] + [TestCase("1. i")] + [TestCase("1. i ")] + [TestCase("1. i ")] + + [TestCase("1. i1\n2. i2")] + [TestCase("1. i1\n2. i2\n a. i2.1")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs new file mode 100644 index 000000000..a23fda9fe --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -0,0 +1,65 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestParagraph + { + [TestCase("p")] + [TestCase(" p")] + [TestCase("p ")] + [TestCase(" p ")] + + [TestCase("p\np")] + [TestCase(" p\np")] + [TestCase("p \np")] + [TestCase(" p \np")] + + [TestCase("p\n p")] + [TestCase(" p\n p")] + [TestCase("p \n p")] + [TestCase(" p \n p")] + + [TestCase("p\np ")] + [TestCase(" p\np ")] + [TestCase("p \np ")] + [TestCase(" p \np ")] + + [TestCase("p\n\n p ")] + [TestCase(" p\n\n p ")] + [TestCase("p \n\n p ")] + [TestCase(" p \n\n p ")] + + [TestCase("p\n\np")] + [TestCase(" p\n\np")] + [TestCase("p \n\np")] + [TestCase(" p \n\np")] + + [TestCase("p\n\n p")] + [TestCase(" p\n\n p")] + [TestCase("p \n\n p")] + [TestCase(" p \n\n p")] + + [TestCase("p\n\np ")] + [TestCase(" p\n\np ")] + [TestCase("p \n\np ")] + [TestCase(" p \n\np ")] + + [TestCase("p\n\n p ")] + [TestCase(" p\n\n p ")] + [TestCase("p \n\n p ")] + [TestCase(" p \n\n p ")] + + // special cases + [TestCase(" p \n\n\n\n p \n\n")] + [TestCase("\np")] + [TestCase("\n\np")] + [TestCase("p\n")] + [TestCase("p\n\n")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs new file mode 100644 index 000000000..6032f8931 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -0,0 +1,218 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestQuoteBlock + { + [TestCase(">q")] + [TestCase(" >q")] + [TestCase(" >q")] + [TestCase(" >q")] + [TestCase("> q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase("> q")] + [TestCase(" > q")] + [TestCase(" > q")] + [TestCase(" > q")] + + [TestCase(">q\n>q")] + [TestCase(">q\n >q")] + [TestCase(">q\n >q")] + [TestCase(">q\n >q")] + [TestCase(">q\n> q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n> q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + [TestCase(">q\n > q")] + + [TestCase(" >q\n>q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + + [TestCase(" >q\n>q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n >q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n> q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + [TestCase(" >q\n > q")] + + [TestCase("> q\n>q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase("> q\n>q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n >q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n> q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + [TestCase("> q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(" > q\n>q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n >q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n> q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + [TestCase(" > q\n > q")] + + [TestCase(">q\n>q\n>q")] + [TestCase(">q\n>\n>q")] + [TestCase(">q\np\n>q")] + [TestCase(">q\n>\n>\n>q")] + [TestCase(">q\n>\n>\n>\n>q")] + [TestCase(">q\n>\n>q\n>\n>q")] + [TestCase("> **q**\n>p\n")] + [TestCase("p\n\n> **q**\n>p\n")] + + [TestCase("> q\np\n> q")] // lazy + [TestCase("> q\n> q\np")] // lazy + + [TestCase(">>q")] + [TestCase(" > > q")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase(" > q")] + [TestCase(" > \tq")] + public void TestCodeBlock(string value) + { + RoundTrip(value); + } + + [TestCase("\n> q")] + [TestCase("\n> q\n")] + [TestCase("\n> q\n\n")] + [TestCase("> q\n\np")] + [TestCase("p\n\n> q\n\n# h")] + + //https://github.com/lunet-io/markdig/issues/480 + //[TestCase(">\np")] + //[TestCase(">**b**\n>\n>p\n>\np\n")] + public void TestParagraph(string value) + { + RoundTrip(value); + } + + [TestCase("> q\n\n# h\n")] + public void TestAtxHeader(string value) + { + RoundTrip(value); + } + + [TestCase(">- i1\n>- i2\n")] + [TestCase("> **p** p\n>- i1\n>- i2\n")] + public void TestUnorderedList(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs new file mode 100644 index 000000000..03ece89fb --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestSetextHeading + { + [TestCase("h1===\n")] + [TestCase("h2---\n")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs new file mode 100644 index 000000000..16e283bab --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestThematicBreak + { + [TestCase("---")] + [TestCase(" ---")] + [TestCase(" ---")] + [TestCase(" ---")] + [TestCase("--- ")] + [TestCase(" --- ")] + [TestCase(" --- ")] + [TestCase(" --- ")] + [TestCase("- - -")] + [TestCase(" - - -")] + [TestCase(" - - - ")] + [TestCase("-- -")] + [TestCase("---\n")] + [TestCase("---\n\n")] + [TestCase("---\np")] + [TestCase("---\n\np")] + [TestCase("---\n# h")] + //[TestCase("p\n\n---")] + /// Note: "p\n---" is parsed as setext heading + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs new file mode 100644 index 000000000..8669318c4 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -0,0 +1,78 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestUnorderedList + { + // i = item + [TestCase("- i1")] + [TestCase("- i1 ")] + [TestCase("- i1\n")] + [TestCase("- i1\n\n")] + [TestCase("- i1\n- i2")] + [TestCase("- i1\n - i2")] + [TestCase("- i1\n - i1.1\n - i1.2")] + [TestCase("- i1 \n- i2 \n")] + [TestCase("- i1 \n- i2 \n")] + [TestCase(" - i1")] + [TestCase(" - i1")] + [TestCase(" - i1")] + [TestCase("- i1\n\n- i1")] + [TestCase("- i1\n\n\n- i1")] + [TestCase("- i1\n - i1.1\n - i1.1.1\n")] + + [TestCase("-\ti1")] + [TestCase("-\ti1\n-\ti2")] + [TestCase("-\ti1\n- i2\n-\ti3")] + public void Test_UnorderedList(string value) + { + RoundTrip(value); + } + + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase("- > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q")] + [TestCase(" - > q1\n - > q2")] + public void TestBlockQuote(string value) + { + RoundTrip(value); + } + + [TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline + [TestCase("- i1\n\n\np\n")] + [TestCase("- i1\n\np")] + [TestCase("- i1\n\np\n")] + public void TestParagraph(string value) + { + RoundTrip(value); + } + + [TestCase("- i1\n\n---\n")] + [TestCase("- i1\n\n\n---\n")] + public void TestThematicBreak(string value) + { + RoundTrip(value); + } + + [TestCase("- i1\n - i1.1\n ```\n code\n ```")] + public void TestCodeBlock(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/TestCst.cs b/src/Markdig.Tests/TestCst.cs deleted file mode 100644 index 435866782..000000000 --- a/src/Markdig.Tests/TestCst.cs +++ /dev/null @@ -1,706 +0,0 @@ -using Markdig.Renderers; -using Markdig.Renderers.Normalize; -using Markdig.Syntax; -using NUnit.Framework; -using System.IO; - -/// -/// General notes -/// - whitespace can occur before, between and after symbols -/// -/// TODO: -/// - \r\n, \r, \n -/// - \t and spaces -/// - html entities i.e. > -/// -namespace Markdig.Tests -{ - [TestFixture] - public class TestCst - { - [Test] - public void TestWhitespaceBefore() - { - var markdown = " This is a paragraph. "; - - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as ParagraphBlock; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - [Test] - public void TestNewLinesBeforeAndAfter() - { - var markdown = " \n \nLine2\n\nLine1\n\n"; - //var markdown = "\r\nLine2\r\n\r\nLine1\r\n\r\n"; - - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as ParagraphBlock; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - private void RoundTrip(string markdown) - { - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - - [TestCase("p")] - [TestCase(" p")] - [TestCase("p ")] - [TestCase(" p ")] - - [TestCase("p\np")] - [TestCase(" p\np")] - [TestCase("p \np")] - [TestCase(" p \np")] - - [TestCase("p\n p")] - [TestCase(" p\n p")] - [TestCase("p \n p")] - [TestCase(" p \n p")] - - [TestCase("p\np ")] - [TestCase(" p\np ")] - [TestCase("p \np ")] - [TestCase(" p \np ")] - - [TestCase("p\n\n p ")] - [TestCase(" p\n\n p ")] - [TestCase("p \n\n p ")] - [TestCase(" p \n\n p ")] - - [TestCase("p\n\np")] - [TestCase(" p\n\np")] - [TestCase("p \n\np")] - [TestCase(" p \n\np")] - - [TestCase("p\n\n p")] - [TestCase(" p\n\n p")] - [TestCase("p \n\n p")] - [TestCase(" p \n\n p")] - - [TestCase("p\n\np ")] - [TestCase(" p\n\np ")] - [TestCase("p \n\np ")] - [TestCase(" p \n\np ")] - - [TestCase("p\n\n p ")] - [TestCase(" p\n\n p ")] - [TestCase("p \n\n p ")] - [TestCase(" p \n\n p ")] - - // special cases - [TestCase(" p \n\n\n\n p \n\n")] - [TestCase("\np")] - [TestCase("\n\np")] - [TestCase("p\n")] - [TestCase("p\n\n")] - public void TestParagraph(string value) - { - RoundTrip(value); - } - - [Test] - public void TestNewLinesBeforeAndAfter2() - { - RoundTrip("\n# H1\n\nLine1"); - RoundTrip("\n# H1\n\nLine1\n"); - RoundTrip("\n# H1\n\nLine1\n\n"); - RoundTrip("\n\n# H1\n\nLine1\n\n"); - RoundTrip("\n\n# H1\nLine1\n\n"); - RoundTrip("\n\n# H1\nLine1\n\n"); - } - - // i = item - [TestCase("- i1")] - [TestCase("- i1 ")] - [TestCase("- i1\n")] - [TestCase("- i1\n\n")] - [TestCase("- i1\n- i2")] - [TestCase("- i1\n - i2")] - [TestCase("- i1\n - i1.1\n - i1.2")] - [TestCase("- i1 \n- i2 \n")] - [TestCase("- i1 \n- i2 \n")] - [TestCase(" - i1")] - [TestCase(" - i1")] - [TestCase(" - i1")] - [TestCase("- i1\n\n- i1")] - [TestCase("- i1\n\n\n- i1")] - [TestCase("- i1\n - i1.1\n - i1.1.1\n")] - - [TestCase("-\ti1")] - [TestCase("-\ti1\n-\ti2")] - [TestCase("-\ti1\n- i2\n-\ti3")] - public void TestUnorderedList(string value) - { - RoundTrip(value); - } - - [TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline - [TestCase("- i1\n\n\np\n")] - [TestCase("- i1\n\np")] - [TestCase("- i1\n\np\n")] - public void TestUnorderedList_Paragraph(string value) - { - RoundTrip(value); - } - - [TestCase("- i1\n\n---\n")] - [TestCase("- i1\n\n\n---\n")] - public void TestUnorderedList_ThematicBreak(string value) - { - RoundTrip(value); - } - - [TestCase("- i1\n - i1.1\n ```\n code\n ```")] - public void TestUnorderedList_CodeBlock(string value) - { - RoundTrip(value); - } - - [TestCase("1. i")] - [TestCase("1. i")] - [TestCase("1. i ")] - [TestCase("1. i ")] - - [TestCase("1. i1\n2. i2")] - [TestCase("1. i1\n2. i2\n a. i2.1")] - public void TestOrderedList(string value) - { - RoundTrip(value); - } - - [TestCase(" ![description](http://example.com)")] - [TestCase("paragraph ![description](http://example.com)")] - public void TestImage2(string value) - { - RoundTrip(value); - } - - [TestCase("# h")] - [TestCase("# h ")] - public void TestHeading(string value) - { - RoundTrip(value); - } - - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase("- > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q")] - [TestCase(" - > q1\n - > q2")] - public void TestUnorderedListItem_BlockQuote(string value) - { - RoundTrip(value); - } - - [TestCase(">q")] - [TestCase(" >q")] - [TestCase(" >q")] - [TestCase(" >q")] - [TestCase("> q")] - [TestCase(" > q")] - [TestCase(" > q")] - [TestCase(" > q")] - [TestCase("> q")] - [TestCase(" > q")] - [TestCase(" > q")] - [TestCase(" > q")] - - [TestCase(">q\n>q")] - [TestCase(">q\n >q")] - [TestCase(">q\n >q")] - [TestCase(">q\n >q")] - [TestCase(">q\n> q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - [TestCase(">q\n> q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - [TestCase(">q\n > q")] - - [TestCase(" >q\n>q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - - [TestCase(" >q\n>q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n >q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n> q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - [TestCase(" >q\n > q")] - - [TestCase("> q\n>q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - - [TestCase("> q\n>q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n >q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n> q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - [TestCase("> q\n > q")] - - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - - [TestCase(" > q\n>q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n >q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n> q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - [TestCase(" > q\n > q")] - - [TestCase(">q\n>q\n>q")] - [TestCase(">q\n>\n>q")] - [TestCase(">q\np\n>q")] - [TestCase(">q\n>\n>\n>q")] - [TestCase(">q\n>\n>\n>\n>q")] - [TestCase(">q\n>\n>q\n>\n>q")] - [TestCase("> **q**\n>p\n")] - [TestCase("p\n\n> **q**\n>p\n")] - - [TestCase("> q\np\n> q")] // lazy - [TestCase("> q\n> q\np")] // lazy - - [TestCase(">>q")] - [TestCase(" > > q")] - public void TestBlockQuote(string value) - { - RoundTrip(value); - } - - [TestCase("---")] - [TestCase(" ---")] - [TestCase(" ---")] - [TestCase(" ---")] - [TestCase("--- ")] - [TestCase(" --- ")] - [TestCase(" --- ")] - [TestCase(" --- ")] - [TestCase("- - -")] - [TestCase(" - - -")] - [TestCase(" - - - ")] - [TestCase("-- -")] - [TestCase("---\n")] - [TestCase("---\n\n")] - [TestCase("---\np")] - [TestCase("---\n\np")] - [TestCase("---\n# h")] - //[TestCase("p\n\n---")] - /// Note: "p\n---" is parsed as setext heading - public void TestThematicBreak(string value) - { - RoundTrip(value); - } - - [TestCase("\n> q")] - [TestCase("\n> q\n")] - [TestCase("\n> q\n\n")] - [TestCase("> q\n\np")] - [TestCase("p\n\n> q\n\n# h")] - - //https://github.com/lunet-io/markdig/issues/480 - //[TestCase(">\np")] - //[TestCase(">**b**\n>\n>p\n>\np\n")] - public void TestBlockQuote_Paragraph(string value) - { - RoundTrip(value); - } - - [TestCase("> q\n\n# h\n")] - public void TestBlockQuote_Header(string value) - { - RoundTrip(value); - } - - [TestCase(">- i1\n>- i2\n")] - [TestCase("> **p** p\n>- i1\n>- i2\n")] - public void TestBlockQuote_ListBlock(string value) - { - RoundTrip(value); - } - - [Test] - public void TestBlockQuote() - { - //RoundTrip("- >quote"); // par in qb in l in li - // 3ws? - ws 3ws? q ws? p - // 3ws? lb - // - li - // ws li - // 3ws? qb - // q qb - // ws? qb - // p p - //RoundTrip(" - > quote"); // par in qb in l in li - } - - /// A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content. - [TestCase(" code")] - [TestCase(" code")] - public void TestImplicitCodeBlock(string value) - { - RoundTrip(value); - } - - [TestCase("[a]")] // TODO: this is not a link but a paragraph - [TestCase("[a]()")] - - [TestCase("[](b)")] - [TestCase(" [](b)")] - [TestCase("[](b) ")] - [TestCase(" [](b) ")] - - [TestCase("[a](b)")] - [TestCase(" [a](b)")] - [TestCase("[a](b) ")] - [TestCase(" [a](b) ")] - - [TestCase("[ a](b)")] - [TestCase(" [ a](b)")] - [TestCase("[ a](b) ")] - [TestCase(" [ a](b) ")] - - [TestCase("[a ](b)")] - [TestCase(" [a ](b)")] - [TestCase("[a ](b) ")] - [TestCase(" [a ](b) ")] - - [TestCase("[ a ](b)")] - [TestCase(" [ a ](b)")] - [TestCase("[ a ](b) ")] - [TestCase(" [ a ](b) ")] - - // below cases are required for a full CST but not have low prio for impl - //[TestCase("[]( b)")] - //[TestCase(" []( b)")] - //[TestCase("[]( b) ")] - //[TestCase(" []( b) ")] - - //[TestCase("[a]( b)")] - //[TestCase(" [a]( b)")] - //[TestCase("[a]( b) ")] - //[TestCase(" [a]( b) ")] - - //[TestCase("[ a]( b)")] - //[TestCase(" [ a]( b)")] - //[TestCase("[ a]( b) ")] - //[TestCase(" [ a]( b) ")] - - //[TestCase("[a ]( b)")] - //[TestCase(" [a ]( b)")] - //[TestCase("[a ]( b) ")] - //[TestCase(" [a ]( b) ")] - - //[TestCase("[ a ]( b)")] - //[TestCase(" [ a ]( b)")] - //[TestCase("[ a ]( b) ")] - //[TestCase(" [ a ]( b) ")] - - //[TestCase("[](b )")] - //[TestCase(" [](b )")] - //[TestCase("[](b ) ")] - //[TestCase(" [](b ) ")] - - //[TestCase("[a](b )")] - //[TestCase(" [a](b )")] - //[TestCase("[a](b ) ")] - //[TestCase(" [a](b ) ")] - - //[TestCase("[ a](b )")] - //[TestCase(" [ a](b )")] - //[TestCase("[ a](b ) ")] - //[TestCase(" [ a](b ) ")] - - //[TestCase("[a ](b )")] - //[TestCase(" [a ](b )")] - //[TestCase("[a ](b ) ")] - //[TestCase(" [a ](b ) ")] - - //[TestCase("[ a ](b )")] - //[TestCase(" [ a ](b )")] - //[TestCase("[ a ](b ) ")] - //[TestCase(" [ a ](b ) ")] - - //[TestCase("[]( b )")] - //[TestCase(" []( b )")] - //[TestCase("[]( b ) ")] - //[TestCase(" []( b ) ")] - - //[TestCase("[a]( b )")] - //[TestCase(" [a]( b )")] - //[TestCase("[a]( b ) ")] - //[TestCase(" [a]( b ) ")] - - //[TestCase("[ a]( b )")] - //[TestCase(" [ a]( b )")] - //[TestCase("[ a]( b ) ")] - //[TestCase(" [ a]( b ) ")] - - //[TestCase("[a ]( b )")] - //[TestCase(" [a ]( b )")] - //[TestCase("[a ]( b ) ")] - //[TestCase(" [a ]( b ) ")] - - //[TestCase("[ a ]( b )")] - //[TestCase(" [ a ]( b )")] - //[TestCase("[ a ]( b ) ")] - //[TestCase(" [ a ]( b ) ")] - public void TestInlineLink(string value) - { - RoundTrip(value); - } - - [TestCase("![](a)")] - [TestCase(" ![](a)")] - [TestCase("![](a) ")] - [TestCase(" ![](a) ")] - public void TestImage(string value) - { - RoundTrip(value); - } - - [TestCase("``")] - [TestCase(" ``")] - [TestCase("`` ")] - [TestCase(" `` ")] - - [TestCase("`a`")] - [TestCase(" `a`")] - [TestCase("`a` ")] - [TestCase(" `a` ")] - - [TestCase("` a`")] - [TestCase(" ` a`")] - [TestCase("` a` ")] - [TestCase(" ` a` ")] - - [TestCase("`a `")] - [TestCase(" `a `")] - [TestCase("`a ` ")] - [TestCase(" `a ` ")] - - [TestCase("``a``")] - [TestCase("- ```a```")] - [TestCase("p ```a``` p")] - - // broken - //[TestCase("```a```")] - [TestCase("```a``` p")] - [TestCase("```a`b`c```")] - //[TestCase("p\n\n```a``` p")] - //[TestCase("```a``` p\n```a``` p")] - - /// : intentionally trimmed. TODO: decide on how to handle - //[TestCase("` a `")] - //[TestCase(" ` a `")] - //[TestCase("` a ` ")] - //[TestCase(" ` a ` ")] - public void TestCodeInline(string value) - { - RoundTrip(value); - } - - [TestCase("")] - [TestCase(" ")] - [TestCase(" ")] - [TestCase(" ")] - - [TestCase("< http://a>")] - [TestCase(" < http://a>")] - [TestCase("< http://a> ")] - [TestCase(" < http://a> ")] - - [TestCase("")] - [TestCase(" ")] - [TestCase(" ")] - [TestCase(" ")] - - [TestCase("< http://a >")] - [TestCase(" < http://a >")] - [TestCase("< http://a > ")] - [TestCase(" < http://a > ")] - public void TestAutolinkInline(string value) - { - RoundTrip(value); - } - - [TestCase("```c```")] - - [TestCase(" l")] - [TestCase("\tl")] - [TestCase("\tl1\n l1")] - - [TestCase("```\nc\n```")] - [TestCase("```\nc\n```\n")] - [TestCase("\n```\nc\n```")] - [TestCase("\n\n```\nc\n```")] - [TestCase("```\nc\n```\n")] - [TestCase("```\nc\n```\n\n")] - [TestCase("\n```\nc\n```\n")] - [TestCase("\n```\nc\n```\n\n")] - [TestCase("\n\n```\nc\n```\n")] - [TestCase("\n\n```\nc\n```\n\n")] - - [TestCase("```\nc\n````")] - [TestCase("p\n\n```\nc\n```")] - public void TestCodeBlock(string value) - { - RoundTrip(value); - } - - [TestCase("
")] - [TestCase("
\n")] - [TestCase("
\n\n")] - [TestCase("
\n\n# h")] - [TestCase("p\n\n
\n")] - [TestCase("
\n\n# h")] - public void TestHtml(string value) - { - RoundTrip(value); - } - - [TestCase("h1===\n")] - [TestCase("h2---\n")] - public void TestSetextHeading(string value) - { - RoundTrip(value); - } - } -} diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 8fc9c1bb0..872f5f4c6 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Markdig.Helpers; using Markdig.Syntax; @@ -735,7 +736,6 @@ private void TryOpenBlocks() if (TryOpenBlocks(globalParsers)) { RestartIndent(); - //RestartBeforeLines(); continue; } } @@ -807,9 +807,16 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (!result.IsDiscard()) { + // TODO: RTP: pass line with whitespace paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); } - + // TODO: RTP: delegate this to container parser classes + var qb = paragraph.Parent as QuoteBlock; + if (qb != null) + { + var afterWhitespace = PopBeforeWhitespace(Start - 1); + qb.QuoteLines.Last().AfterWhitespace = afterWhitespace; + } // We have just found a lazy continuation for a paragraph, early exit // Mark all block opened after a lazy continuation OpenAll(); diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 4fff52fd4..70fde42d7 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -79,7 +79,7 @@ public override BlockState TryOpen(BlockProcessor processor) Level = leadingCount, Column = column, Span = { Start = sourcePosition }, - BeforeWhitespace = processor.PopBeforeWhitespace(column), + BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), LinesBefore = processor.UseLinesBefore() }; processor.NewBlocks.Push(headingBlock); diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 230e3fe6d..7c3d62e85 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -53,7 +53,7 @@ public override BlockState TryOpen(BlockProcessor processor) }; quoteBlock.QuoteLines.Add(new QuoteBlock.QuoteLine { - BeforeWhitespace = processor.PopBeforeWhitespace(processor.ColumnBeforeIndent, sourcePosition - 1), + BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), QuoteChar = true }); processor.NewBlocks.Push(quoteBlock); diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 408975c53..7d09de36e 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -19,7 +19,8 @@ protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragra renderer.RenderLinesBefore(paragraph); renderer.Write(paragraph.BeforeWhitespace); renderer.WriteLeafInline(paragraph); - renderer.FinishBlock(); + //renderer.FinishBlock(); + renderer.RenderLineAfterIfNeeded(paragraph); renderer.RenderLinesAfter(paragraph); } } diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 85d2ed4ad..bfecd2c94 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -24,7 +24,7 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) var indents = new List(); foreach (var quoteLine in quoteBlock.QuoteLines) { - indents.Add(quoteLine.BeforeWhitespace.ToString() + (quoteLine.QuoteChar ? ">" : "")); + indents.Add(quoteLine.BeforeWhitespace.ToString() + (quoteLine.QuoteChar ? ">" : "") + quoteLine.AfterWhitespace.ToString()); } renderer.PushIndent(indents); diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index cb585f936..aa0fb6367 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -75,16 +75,16 @@ private class Indent private readonly string _constant; private readonly Queue _lineSpecific; - public Indent(string constant) + internal Indent(string constant) { _constant = constant; } - public Indent(IEnumerable lineSpecific) + internal Indent(IEnumerable lineSpecific) { _lineSpecific = new Queue(lineSpecific); } - public string Next() + internal string Next() { if (_constant != null) { diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index 6cbb9c25c..a02f466dd 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -69,6 +69,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi else { // We need to expand tabs to spaces + // TODO: RTP: also register tabs here var builder = StringBuilderCache.Local(); builder.Append(' ', CharHelper.AddTab(column) - column); builder.Append(slice.Text, slice.Start + 1, slice.Length - 1); diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index a185a9e1a..730835491 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -19,6 +19,8 @@ public class QuoteLine { public StringSlice BeforeWhitespace { get; set; } + public StringSlice AfterWhitespace { get; set; } + // support lazy lines public bool QuoteChar { get; set; } } From 0b79389b148221b32601ca8a5571caf660ee2113 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 4 Oct 2020 21:49:54 +0200 Subject: [PATCH 027/120] fix CodeInline parsing & rendering --- .../RoundtripSpecs/Inlines/TestCodeInline.cs | 53 +++++++++++-------- .../RoundtripSpecs/TestUnorderedList.cs | 1 + src/Markdig/Parsers/FencedBlockParserBase.cs | 4 ++ .../Parsers/Inlines/CodeInlineParser.cs | 5 +- .../Normalize/Inlines/CodeInlineRenderer.cs | 8 +++ .../Renderers/Normalize/ParagraphRenderer.cs | 1 - src/Markdig/Syntax/Inlines/CodeInline.cs | 2 + 7 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs index 7fe63124a..6e6c9a1df 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs @@ -11,26 +11,28 @@ public class TestCodeInline [TestCase("`` ")] [TestCase(" `` ")] - [TestCase("`a`")] - [TestCase(" `a`")] - [TestCase("`a` ")] - [TestCase(" `a` ")] - - [TestCase("` a`")] - [TestCase(" ` a`")] - [TestCase("` a` ")] - [TestCase(" ` a` ")] - - [TestCase("`a `")] - [TestCase(" `a `")] - [TestCase("`a ` ")] - [TestCase(" `a ` ")] - - [TestCase("``a``")] - [TestCase("- ```a```")] - [TestCase("p ```a``` p")] - + [TestCase("`c`")] + [TestCase(" `c`")] + [TestCase("`c` ")] + [TestCase(" `c` ")] + + [TestCase("` c`")] + [TestCase(" ` c`")] + [TestCase("` c` ")] + [TestCase(" ` c` ")] + + [TestCase("`c `")] + [TestCase(" `c `")] + [TestCase("`c ` ")] + [TestCase(" `c ` ")] + + [TestCase("``c``")] [TestCase("```c```")] + [TestCase("````c````")] + + [TestCase("p `a` p")] + [TestCase("p ``a`` p")] + [TestCase("p ```a``` p")] // broken //[TestCase("```a```")] @@ -41,10 +43,15 @@ public class TestCodeInline /// : intentionally trimmed. TODO: decide on how to handle //[TestCase("` a `")] - //[TestCase(" ` a `")] - //[TestCase("` a ` ")] - //[TestCase(" ` a ` ")] - public void Test_CodeInline(string value) + [TestCase(" ` a `")] + [TestCase("` a ` ")] + [TestCase(" ` a ` ")] + public void Test(string value) + { + RoundTrip(value); + } + + public void TestParagraph(string value) { RoundTrip(value); } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index 8669318c4..83aa2afa6 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -69,6 +69,7 @@ public void TestThematicBreak(string value) RoundTrip(value); } + [TestCase("- ```a```")] [TestCase("- i1\n - i1.1\n ```\n code\n ```")] public void TestCodeBlock(string value) { diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index b56502bed..572d0193d 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -95,6 +95,10 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice for (int i = line.Start; i <= line.End; i++) { char c = line.Text[i]; + if (c == '`') + { + return false; + } switch (state) { case ParseState.AfterFence: diff --git a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs index 00cbff4ee..03de579a9 100644 --- a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs @@ -87,6 +87,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } bool isMatching = false; + bool firstAndLastWasSpace = false; if (closeSticks == openSticks) { string content; @@ -95,6 +96,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (!allSpace && builder.Length > 2 && builder[0] == ' ' && builder[builder.Length - 1] == ' ') { content = builder.ToString(1, builder.Length - 2); + firstAndLastWasSpace = true; } else { @@ -109,7 +111,8 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), Line = line, Column = column, - DelimiterCount = delimiterCount + DelimiterCount = delimiterCount, + FirstAndLastWasSpace = firstAndLastWasSpace }; isMatching = true; } diff --git a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs index e7a8aab9a..b1fdce46b 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs @@ -32,6 +32,10 @@ protected override void Write(NormalizeRenderer renderer, CodeInline obj) //} var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); renderer.Write(delimiterRun); + if (obj.FirstAndLastWasSpace) + { + renderer.Write(' '); + } if (obj.Content.Length != 0) { if (obj.Content[0] == obj.Delimiter) @@ -48,6 +52,10 @@ protected override void Write(NormalizeRenderer renderer, CodeInline obj) { renderer.Write(' '); } + if (obj.FirstAndLastWasSpace) + { + renderer.Write(' '); + } renderer.Write(delimiterRun); } } diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 7d09de36e..5d02108f5 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -19,7 +19,6 @@ protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragra renderer.RenderLinesBefore(paragraph); renderer.Write(paragraph.BeforeWhitespace); renderer.WriteLeafInline(paragraph); - //renderer.FinishBlock(); renderer.RenderLineAfterIfNeeded(paragraph); renderer.RenderLinesAfter(paragraph); } diff --git a/src/Markdig/Syntax/Inlines/CodeInline.cs b/src/Markdig/Syntax/Inlines/CodeInline.cs index 1aae6a7b0..0d0bb63c5 100644 --- a/src/Markdig/Syntax/Inlines/CodeInline.cs +++ b/src/Markdig/Syntax/Inlines/CodeInline.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using System.Diagnostics; namespace Markdig.Syntax.Inlines @@ -28,5 +29,6 @@ public class CodeInline : LeafInline ///
public string Content { get; set; } + public bool FirstAndLastWasSpace { get; set; } } } \ No newline at end of file From 340b75b5575ade9fdc7d616d0f9a2b419bdad6cf Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 9 Oct 2020 13:24:44 +0200 Subject: [PATCH 028/120] add some tests, fix nested blockquote in listitem --- .../Inlines/TestBackslashEscape.cs | 10 ++++ .../Inlines/TestHtmlEntities.cs | 22 +++++++++ .../Inlines/TestLineBreakInline.cs | 18 +++++++ src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 13 +++++ .../RoundtripSpecs/TestParagraph.cs | 48 ++++++++++++++++++- .../RoundtripSpecs/TestQuoteBlock.cs | 5 ++ .../RoundtripSpecs/TestUnorderedList.cs | 4 +- src/Markdig/Parsers/ListBlockParser.cs | 4 +- .../Renderers/Normalize/ListRenderer.cs | 10 ++-- .../Renderers/Normalize/QuoteBlockRenderer.cs | 5 -- src/Markdig/Renderers/TextRendererBase.cs | 3 +- 11 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Roundtrip.md diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs new file mode 100644 index 000000000..760b92a3a --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + class TestBackslashEscape + { + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs new file mode 100644 index 000000000..7c9af3363 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + /// + /// + /// + /// + [TestFixture] + public class TestHtmlEntities + { + [TestCase(">")] + [TestCase("<")] + [TestCase(" ")] + [TestCase("♥")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs new file mode 100644 index 000000000..866f20e25 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestLineBreakInline + { + [TestCase("p\n")] + [TestCase("p\r\n")] + [TestCase("p\r")] + [TestCase("[]() ![]() `` ` ` ` ` ![]() ![]()")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md new file mode 100644 index 000000000..81f01a526 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -0,0 +1,13 @@ +# Roundtrip support +Roundtrip support allows parsing of Markdown to subsequently render it back to Markdown without changes. This requires storing all characters on the parse tree, including whitespace and special characters. This document outlines decisions and guidelines on how these characters are stored. + +# Guidelines +- newlines before blocks are assigned to that block +- whitespace starting on a line is assigned to the block on that line + +## Quoteblock +Quoteblocks may have different syntactical characters applied per line. That is, some lines belonging to a Quoteblock may and others **may not** contain the quote marker character `>`. Each line of a Quoteblock therefore stores the quote marker character and its surrounding whitespace. + +## Lists +- beforewhitespace on list item +- \ No newline at end of file diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index a23fda9fe..af5f031a4 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -51,12 +51,58 @@ public class TestParagraph [TestCase("p \n\n p ")] [TestCase(" p \n\n p ")] + [TestCase("\np")] + [TestCase("\n p")] + [TestCase("\np ")] + [TestCase("\n p ")] + + [TestCase("\np\np")] + [TestCase("\n p\np")] + [TestCase("\np \np")] + [TestCase("\n p \np")] + + [TestCase("\np\n p")] + [TestCase("\n p\n p")] + [TestCase("\np \n p")] + [TestCase("\n p \n p")] + + [TestCase("\np\np ")] + [TestCase("\n p\np ")] + [TestCase("\np \np ")] + [TestCase("\n p \np ")] + + [TestCase("\np\n\n p ")] + [TestCase("\n p\n\n p ")] + [TestCase("\np \n\n p ")] + [TestCase("\n p \n\n p ")] + + [TestCase("\np\n\np")] + [TestCase("\n p\n\np")] + [TestCase("\np \n\np")] + [TestCase("\n p \n\np")] + + [TestCase("\np\n\n p")] + [TestCase("\n p\n\n p")] + [TestCase("\np \n\n p")] + [TestCase("\n p \n\n p")] + + [TestCase("\np\n\np ")] + [TestCase("\n p\n\np ")] + [TestCase("\np \n\np ")] + [TestCase("\n p \n\np ")] + + [TestCase("\np\n\n p ")] + [TestCase("\n p\n\n p ")] + [TestCase("\np \n\n p ")] + [TestCase("\n p \n\n p ")] + // special cases [TestCase(" p \n\n\n\n p \n\n")] - [TestCase("\np")] [TestCase("\n\np")] [TestCase("p\n")] [TestCase("p\n\n")] + [TestCase("p\np\n p")] + [TestCase("p\np\n p\n p")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 6032f8931..8a56cf08d 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -208,6 +208,11 @@ public void TestAtxHeader(string value) RoundTrip(value); } + [TestCase(">- i")] + [TestCase("> - i")] + [TestCase(">- i\n>- i")] + [TestCase(">- >p")] + [TestCase("> - >p")] [TestCase(">- i1\n>- i2\n")] [TestCase("> **p** p\n>- i1\n>- i2\n")] public void TestUnorderedList(string value) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index 83aa2afa6..ea26ff1a7 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -26,7 +26,7 @@ public class TestUnorderedList [TestCase("-\ti1")] [TestCase("-\ti1\n-\ti2")] [TestCase("-\ti1\n- i2\n-\ti3")] - public void Test_UnorderedList(string value) + public void Test(string value) { RoundTrip(value); } @@ -71,6 +71,8 @@ public void TestThematicBreak(string value) [TestCase("- ```a```")] [TestCase("- i1\n - i1.1\n ```\n code\n ```")] + [TestCase("- c")] + [TestCase("- c\n c")] public void TestCodeBlock(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index b3cf892d6..02eeb0124 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -208,7 +208,9 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) return BlockState.None; } var bulletLength = 1; // TODO: fix for ordered - var whitespaceBefore = state.PopBeforeWhitespace(state.CurrentLineStartPosition, state.Start - 1 - bulletLength); // -1: + var whitespaceBefore = state.PopBeforeWhitespace(state.StartBeforeIndent, state.Start - 1 - bulletLength); // -1: + //var whitespaceBefore = state.PopBeforeWhitespace(state.CurrentLineStartPosition, state.Start - 1 - bulletLength); // -1: + //var whitespaceBefore = state.PopBeforeWhitespace(sourcePosition, state.Start - 1 - bulletLength); // -1: int whitespaceAfterStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 4602529ac..18ac53d7d 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using System.Collections.Generic; using System.Globalization; +using Markdig.Helpers; using Markdig.Syntax; namespace Markdig.Renderers.Normalize @@ -73,12 +75,14 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) var item = listBlock[i]; var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); - renderer.Write(listItem.BeforeWhitespace); - renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); - renderer.Write(listItem.AfterWhitespace); + StringSlice bws = listItem.BeforeWhitespace; + char bullet = renderer.Options.ListItemCharacter ?? listBlock.BulletType; + StringSlice aws = listItem.AfterWhitespace; + renderer.PushIndent(new List { $@"{bws}{bullet}{aws}" }); renderer.WriteChildren(listItem); + renderer.PopIndent(); renderer.RenderLinesAfter(listItem); writeLine = true; diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index bfecd2c94..79ed21b03 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -18,9 +18,6 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) renderer.RenderLinesBefore(quoteBlock); renderer.Write(quoteBlock.BeforeWhitespace); - //var quoteIndent = quoteBlock.HasSpaceAfterQuoteChar ? quoteBlock.QuoteChar + " " : quoteBlock.QuoteChar.ToString(); - //var quoteIndent = quoteBlock.QuoteChar.ToString(); - var indents = new List(); foreach (var quoteLine in quoteBlock.QuoteLines) { @@ -28,8 +25,6 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) } renderer.PushIndent(indents); - // indents are not always written, depending on the childs - //renderer.Write(quoteIndent); renderer.WriteChildren(quoteBlock); renderer.PopIndent(); diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index aa0fb6367..7bac70999 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -90,7 +90,8 @@ internal string Next() { return _constant; } - if (_lineSpecific.Count == 0) throw new Exception("Indents empty"); + //if (_lineSpecific.Count == 0) throw new Exception("Indents empty"); + if (_lineSpecific.Count == 0) return string.Empty; var next = _lineSpecific.Dequeue(); return next; } From 9d82088e03c02d834b3fa3c41f6ebc63928696f6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 9 Oct 2020 14:14:41 +0200 Subject: [PATCH 029/120] fix quoteblock --- src/Markdig/Parsers/BlockProcessor.cs | 4 ++-- src/Markdig/Parsers/ListBlockParser.cs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 872f5f4c6..6073b4383 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -183,14 +183,14 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo public StringSlice PopBeforeWhitespace(int end) { var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); - WhitespaceStart = end + 1; + WhitespaceStart = end; return stringSlice; } public StringSlice PopBeforeWhitespace(int start, int end) { var stringSlice = new StringSlice(Line.Text, start, end); - WhitespaceStart = end + 1; + WhitespaceStart = end; return stringSlice; } diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 02eeb0124..6abac5b1f 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -207,10 +207,8 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) state.GoToColumn(initColumn); return BlockState.None; } - var bulletLength = 1; // TODO: fix for ordered - var whitespaceBefore = state.PopBeforeWhitespace(state.StartBeforeIndent, state.Start - 1 - bulletLength); // -1: - //var whitespaceBefore = state.PopBeforeWhitespace(state.CurrentLineStartPosition, state.Start - 1 - bulletLength); // -1: - //var whitespaceBefore = state.PopBeforeWhitespace(sourcePosition, state.Start - 1 - bulletLength); // -1: + var bulletLength = 1; // TODO: RTP: fix for ordered + var whitespaceBefore = state.PopBeforeWhitespace(state.Start - 1 - bulletLength); // -1: int whitespaceAfterStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; From 95fb53cbc9b8185977eca850dad09914f52e8d3c Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 9 Oct 2020 16:44:16 +0200 Subject: [PATCH 030/120] fix IndentedCodeBlock newlines --- .../RoundtripSpecs/TestCodeBlock.cs | 26 +++++++++++++++++-- src/Markdig/Parsers/BlockProcessor.cs | 4 +-- .../Parsers/IndentedCodeBlockParser.cs | 26 +++++++++++++++++-- .../Renderers/Normalize/CodeBlockRenderer.cs | 6 ++--- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs index e0078c49a..2f64fe029 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs @@ -10,8 +10,30 @@ public class TestCodeBlock // l = line [TestCase(" l")] [TestCase(" l")] - [TestCase("\tl")] - [TestCase("\tl1\n l1")] + //[TestCase("\tl")] + //[TestCase("\tl1\n l1")] + + [TestCase("\n l")] + [TestCase("\n\n l")] + [TestCase("\n l\n")] + [TestCase("\n l\n\n")] + [TestCase("\n\n l\n")] + [TestCase("\n\n l\n\n")] + + [TestCase(" l\n l")] + [TestCase(" l\n l\n l")] + + + // two newlines are needed for indented codeblock start after paragraph + [TestCase("p\n\n l")] + [TestCase("p\n\n l\n")] + [TestCase("p\n\n l\n\n")] + + [TestCase("p\n\n l\n l")] + [TestCase("p\n\n l\n l")] + + [TestCase(" l\n\np\n\n l")] + [TestCase(" l l\n\np\n\n l l")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 6073b4383..872f5f4c6 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -183,14 +183,14 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo public StringSlice PopBeforeWhitespace(int end) { var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); - WhitespaceStart = end; + WhitespaceStart = end + 1; return stringSlice; } public StringSlice PopBeforeWhitespace(int start, int end) { var stringSlice = new StringSlice(Line.Text, start, end); - WhitespaceStart = end; + WhitespaceStart = end + 1; return stringSlice; } diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index 780eb43ec..d79757478 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Syntax; +using System.Collections.Generic; namespace Markdig.Parsers { @@ -31,7 +33,8 @@ public override BlockState TryOpen(BlockProcessor processor) processor.NewBlocks.Push(new CodeBlock(this) { Column = processor.Column, - Span = new SourceSpan(processor.Start, processor.Line.End) + Span = new SourceSpan(processor.Start, processor.Line.End), + LinesBefore = processor.UseLinesBefore() }); // Go back to the correct column @@ -42,10 +45,29 @@ public override BlockState TryOpen(BlockProcessor processor) public override BlockState TryContinue(BlockProcessor processor, Block block) { + bool isLastLine = processor.Line.Start == processor.Line.End + 1; // TODO: RTP: meh. Does this also work for \r\n? if (!processor.IsCodeIndent || processor.IsBlankLine) { - if (block == null || !processor.IsBlankLine) + if (block == null || !processor.IsBlankLine || (processor.IsBlankLine && isLastLine)) { + if (block != null) + { + var codeBlock = (CodeBlock)block; + // add trailing blank lines to blank lines stack of processor + for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) + { + if (codeBlock.Lines.Lines[i].Slice.IsEmpty) + { + StringLine line = codeBlock.Lines.Lines[i]; + processor.BeforeLines ??= new List(); + processor.BeforeLines.Add(line.Slice); + } + else + { + break; + } + } + } return BlockState.None; } } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 2220b4db8..c7b50f6c8 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -16,9 +16,9 @@ public class CodeBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, CodeBlock obj) { + renderer.RenderLinesBefore(obj); if (obj is FencedCodeBlock fencedCodeBlock) { - renderer.RenderLinesBefore(obj); var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); renderer.Write(opening); @@ -57,14 +57,14 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.WriteLeafRawLines(obj, true); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); - renderer.RenderLinesAfter(obj); } else { renderer.WriteLeafRawLines(obj, false, true); + renderer.RenderLineAfterIfNeeded(obj); } - renderer.FinishBlock(renderer.Options.EmptyLineAfterCodeBlock); + renderer.RenderLinesAfter(obj); } } } \ No newline at end of file From 9ecb5b995034ef67dbf5960c6015233897aa0690 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 9 Oct 2020 17:20:58 +0200 Subject: [PATCH 031/120] test nested indented codeblock in quoteblock --- src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 8a56cf08d..9f4b72562 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -181,8 +181,15 @@ public void Test(string value) RoundTrip(value); } - [TestCase(" > q")] + [TestCase("> q")] // 5 + [TestCase("> q")] // 6 + [TestCase(" > q")] //5 + [TestCase(" > q")] //6 [TestCase(" > \tq")] + [TestCase("> q\n> q")] // 5, 5 + [TestCase("> q\n> q")] // 5, 6 + [TestCase("> q\n> q")] // 6, 5 + [TestCase("> q\n> q")] // 6, 6 public void TestCodeBlock(string value) { RoundTrip(value); From c9365f355105d054c3bd4e0e8676f99dd7f7ee0a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 9 Oct 2020 18:30:03 +0200 Subject: [PATCH 032/120] correctly render whitespace for IndentedCodeBlock --- ...tCodeBlock.cs => TestIndentedCodeBlock.cs} | 2 +- .../RoundtripSpecs/TestQuoteBlock.cs | 2 +- .../RoundtripSpecs/TestUnorderedList.cs | 12 ++++++-- .../Parsers/IndentedCodeBlockParser.cs | 24 ++++++++++++++-- .../Renderers/Normalize/CodeBlockRenderer.cs | 28 ++++++++++++++++++- src/Markdig/Syntax/CodeBlock.cs | 9 ++++++ 6 files changed, 68 insertions(+), 9 deletions(-) rename src/Markdig.Tests/RoundtripSpecs/{TestCodeBlock.cs => TestIndentedCodeBlock.cs} (96%) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs similarity index 96% rename from src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs rename to src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index 2f64fe029..9ab46f12e 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -4,7 +4,7 @@ namespace Markdig.Tests.RoundtripSpecs { [TestFixture] - public class TestCodeBlock + public class TestIndentedCodeBlock { // A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content. // l = line diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 9f4b72562..4040cb726 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -190,7 +190,7 @@ public void Test(string value) [TestCase("> q\n> q")] // 5, 6 [TestCase("> q\n> q")] // 6, 5 [TestCase("> q\n> q")] // 6, 6 - public void TestCodeBlock(string value) + public void TestIndentedCodeBlock(string value) { RoundTrip(value); } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index ea26ff1a7..e330a4a64 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -69,11 +69,17 @@ public void TestThematicBreak(string value) RoundTrip(value); } - [TestCase("- ```a```")] - [TestCase("- i1\n - i1.1\n ```\n code\n ```")] [TestCase("- c")] [TestCase("- c\n c")] - public void TestCodeBlock(string value) + [TestCase(" - c\n c")] + public void TestIndentedCodeBlock(string value) + { + RoundTrip(value); + } + + [TestCase("- ```a```")] + [TestCase("- i1\n - i1.1\n ```\n code\n ```")] + public void TestFencedCodeBlock(string value) { RoundTrip(value); } diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index d79757478..fe5c9b1b1 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -5,6 +5,7 @@ using Markdig.Helpers; using Markdig.Syntax; using System.Collections.Generic; +using static Markdig.Syntax.CodeBlock; namespace Markdig.Parsers { @@ -26,16 +27,23 @@ public override BlockState TryOpen(BlockProcessor processor) { // Save the column where we need to go back var column = processor.Column; + var sourceStartPosition = processor.Start; // Unwind all indents all spaces before in order to calculate correct span processor.UnwindAllIndents(); - processor.NewBlocks.Push(new CodeBlock(this) + var codeBlock = new CodeBlock(this) { Column = processor.Column, Span = new SourceSpan(processor.Start, processor.Line.End), - LinesBefore = processor.UseLinesBefore() - }); + LinesBefore = processor.UseLinesBefore(), + }; + var codeBlockLine = new CodeBlockLine + { + BeforeWhitespace = processor.PopBeforeWhitespace(sourceStartPosition - 1) + }; + codeBlock.CodeBlockLines.Add(codeBlockLine); + processor.NewBlocks.Push(codeBlock); // Go back to the correct column processor.GoToColumn(column); @@ -80,7 +88,17 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) if (block != null) { block.UpdateSpanEnd(processor.Line.End); + + // lines + var cb = (CodeBlock)block; + var codeBlockLine = new CodeBlockLine + { + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1) + }; + cb.CodeBlockLines ??= new List(); + cb.CodeBlockLines.Add(codeBlockLine); } + return BlockState.Continue; } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index c7b50f6c8..074641ac3 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Syntax; +using System.Collections.Generic; namespace Markdig.Renderers.Normalize { @@ -60,11 +62,35 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) } else { - renderer.WriteLeafRawLines(obj, false, true); + var indents = new List(); + foreach (var cbl in obj.CodeBlockLines) + { + indents.Add(cbl.BeforeWhitespace.ToString()); + } + renderer.PushIndent(indents); + WriteLeafRawLines(renderer, obj); + renderer.PopIndent(); renderer.RenderLineAfterIfNeeded(obj); } renderer.RenderLinesAfter(obj); } + + public void WriteLeafRawLines(NormalizeRenderer renderer, LeafBlock leafBlock) + { + if (leafBlock.Lines.Lines != null) + { + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (int i = 0; i < lines.Count; i++) + { + if (i > 0) + { + renderer.WriteLine(); + } + renderer.Write(ref slices[i].Slice); + } + } + } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/CodeBlock.cs b/src/Markdig/Syntax/CodeBlock.cs index 4fc036e73..6c22c44d9 100644 --- a/src/Markdig/Syntax/CodeBlock.cs +++ b/src/Markdig/Syntax/CodeBlock.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; +using System.Collections.Generic; namespace Markdig.Syntax { @@ -14,6 +16,13 @@ namespace Markdig.Syntax /// public class CodeBlock : LeafBlock { + public class CodeBlockLine + { + public StringSlice BeforeWhitespace { get; set; } + } + + public List CodeBlockLines { get; set; } = new List(); + /// /// Initializes a new instance of the class. /// From f3db5e882e7a2a68b41bcb231908bdfb29c35cc9 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 9 Oct 2020 19:47:16 +0200 Subject: [PATCH 033/120] fix FencedCodeBlock --- src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs | 1 + src/Markdig/Parsers/FencedCodeBlockParser.cs | 2 ++ src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index 87428c7f6..0fe7d799f 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -17,6 +17,7 @@ public class TestFencedCodeBlock [TestCase("\n\n```\nc\n```\n")] [TestCase("\n\n```\nc\n```\n\n")] + [TestCase(" ```\nc\n````")] [TestCase("```\nc\n````")] [TestCase("p\n\n```\nc\n```")] public void Test(string value) diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index 694837c57..376409037 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -4,6 +4,7 @@ using Markdig.Helpers; using Markdig.Syntax; +using System.ComponentModel; namespace Markdig.Parsers { @@ -30,6 +31,7 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) { IndentCount = processor.Indent, LinesBefore = processor.UseLinesBefore(), + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1) }; } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 074641ac3..8bc13cca6 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -21,6 +21,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.RenderLinesBefore(obj); if (obj is FencedCodeBlock fencedCodeBlock) { + renderer.Write(obj.BeforeWhitespace); var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); renderer.Write(opening); @@ -59,6 +60,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.WriteLeafRawLines(obj, true); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); + renderer.Write(obj.AfterWhitespace); } else { From 033f156b2b84e9ced8667383b1d0ffc55cfa5193 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 10 Oct 2020 16:37:17 +0200 Subject: [PATCH 034/120] add many tests, create todolist --- .../Inlines/TestBackslashEscapeInline.cs | 67 +++++++++ .../RoundtripSpecs/Inlines/TestCodeInline.cs | 28 ++-- .../Inlines/TestEmphasisInline.cs | 132 ++++++++++++++++++ .../Inlines/TestHtmlEntities.cs | 22 --- .../Inlines/TestHtmlEntityInline.cs | 53 +++++++ .../RoundtripSpecs/Inlines/TestHtmlInline.cs | 15 ++ .../{TestImage.cs => TestImageInline.cs} | 2 +- ...ackslashEscape.cs => TestLiteralInline.cs} | 2 +- .../Inlines/TestNullCharacterInline.cs | 18 +++ src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 18 ++- .../RoundtripSpecs/TestAtxHeading.cs | 2 +- .../RoundtripSpecs/TestFencedCodeBlock.cs | 1 + .../RoundtripSpecs/TestHtmlBlock.cs | 2 +- .../RoundtripSpecs/TestIndentedCodeBlock.cs | 4 +- .../TestLinkReferenceDefinition.cs | 10 ++ .../RoundtripSpecs/TestOrderedList.cs | 96 ++++++++++++- .../RoundtripSpecs/TestParagraph.cs | 6 + .../RoundtripSpecs/TestSetextHeading.cs | 17 ++- .../RoundtripSpecs/TestUnorderedList.cs | 14 +- .../Normalize/SetextHeadingRenderer.cs | 10 ++ 20 files changed, 470 insertions(+), 49 deletions(-) create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs delete mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs rename src/Markdig.Tests/RoundtripSpecs/Inlines/{TestImage.cs => TestImageInline.cs} (94%) rename src/Markdig.Tests/RoundtripSpecs/Inlines/{TestBackslashEscape.cs => TestLiteralInline.cs} (81%) create mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs create mode 100644 src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs new file mode 100644 index 000000000..bb0e5c5b1 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs @@ -0,0 +1,67 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestBackslashEscapeInline + { + [TestCase(@"\!")] + [TestCase(@"\""")] + [TestCase(@"\#")] + [TestCase(@"\$")] + [TestCase(@"\&")] + [TestCase(@"\'")] + [TestCase(@"\(")] + [TestCase(@"\)")] + [TestCase(@"\*")] + [TestCase(@"\+")] + [TestCase(@"\,")] + [TestCase(@"\-")] + [TestCase(@"\.")] + [TestCase(@"\/")] + [TestCase(@"\:")] + [TestCase(@"\;")] + [TestCase(@"\<")] + [TestCase(@"\=")] + [TestCase(@"\>")] + [TestCase(@"\?")] + [TestCase(@"\@")] + [TestCase(@"\[")] + [TestCase(@"\\")] + [TestCase(@"\]")] + [TestCase(@"\^")] + [TestCase(@"\_")] + [TestCase(@"\`")] + [TestCase(@"\{")] + [TestCase(@"\|")] + [TestCase(@"\}")] + [TestCase(@"\~")] + + // below test breaks visual studio + //[TestCase(@"\!\""\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase(@"# \#\#h1")] + [TestCase(@"# \#\#h1\#")] + public void TestHeading(string value) + { + RoundTrip(value); + } + + [TestCase(@"`\``")] + [TestCase(@"` \``")] + [TestCase(@"`\` `")] + [TestCase(@"` \` `")] + [TestCase(@" ` \` `")] + [TestCase(@"` \` ` ")] + [TestCase(@" ` \` ` ")] + public void TestCodeSpanInline(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs index 6e6c9a1df..9ade5e03b 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs @@ -26,23 +26,23 @@ public class TestCodeInline [TestCase("`c ` ")] [TestCase(" `c ` ")] - [TestCase("``c``")] - [TestCase("```c```")] - [TestCase("````c````")] + [TestCase("`c``")] // 1, 2 + [TestCase("``c`")] // 2, 1 + [TestCase("``c``")] // 2, 2 - [TestCase("p `a` p")] - [TestCase("p ``a`` p")] - [TestCase("p ```a``` p")] + [TestCase("```c``")] // 2, 3 + [TestCase("``c```")] // 3, 2 + [TestCase("```c```")] // 3, 3 + + [TestCase("```c````")] // 3, 4 + [TestCase("````c```")] // 4, 3 + [TestCase("````c````")] // 4, 4 - // broken - //[TestCase("```a```")] [TestCase("```a``` p")] [TestCase("```a`b`c```")] - //[TestCase("p\n\n```a``` p")] - //[TestCase("```a``` p\n```a``` p")] + [TestCase("```a``` p\n```a``` p")] - /// : intentionally trimmed. TODO: decide on how to handle - //[TestCase("` a `")] + [TestCase("` a `")] [TestCase(" ` a `")] [TestCase("` a ` ")] [TestCase(" ` a ` ")] @@ -51,6 +51,10 @@ public void Test(string value) RoundTrip(value); } + [TestCase("p `a` p")] + [TestCase("p ``a`` p")] + [TestCase("p ```a``` p")] + [TestCase("p\n\n```a``` p")] public void TestParagraph(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs new file mode 100644 index 000000000..4e1357383 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs @@ -0,0 +1,132 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestEmphasisInline + { + [TestCase("_t_")] + [TestCase("_t_t")] + [TestCase("t_t_")] + [TestCase("_t t_")] + [TestCase("_t\tt_")] + [TestCase("*t*")] + [TestCase("t*t*")] + [TestCase("*t*t")] + [TestCase("*t t*")] + [TestCase("*t\tt*")] + + [TestCase(" _t_")] + [TestCase(" _t_t")] + [TestCase(" t_t_")] + [TestCase(" _t t_")] + [TestCase(" _t\tt_")] + [TestCase(" *t*")] + [TestCase(" t*t*")] + [TestCase(" *t*t")] + [TestCase(" *t t*")] + [TestCase(" *t\tt*")] + + [TestCase("_t_")] + [TestCase("_t_t ")] + [TestCase("t_t_ ")] + [TestCase("_t t_ ")] + [TestCase("_t\tt_ ")] + [TestCase("*t* ")] + [TestCase("t*t* ")] + [TestCase("*t*t ")] + [TestCase("*t t* ")] + [TestCase("*t\tt* ")] + + [TestCase(" _t_")] + [TestCase(" _t_t ")] + [TestCase(" t_t_ ")] + [TestCase(" _t t_ ")] + [TestCase(" _t\tt_ ")] + [TestCase(" *t* ")] + [TestCase(" t*t* ")] + [TestCase(" *t*t ")] + [TestCase(" *t t* ")] + [TestCase(" *t\tt* ")] + + [TestCase("_t_\t")] + [TestCase("_t_t\t")] + [TestCase("t_t_\t")] + [TestCase("_t t_\t")] + [TestCase("_t\tt_\t")] + [TestCase("*t*\t")] + [TestCase("t*t*\t")] + [TestCase("*t*t\t")] + [TestCase("*t t*\t")] + [TestCase("*t\tt*\t")] + public void Test_Emphasis(string value) + { + RoundTrip(value); + } + + [TestCase("__t__")] + [TestCase("__t__t")] + [TestCase("t__t__")] + [TestCase("__t t__")] + [TestCase("__t\tt__")] + [TestCase("**t**")] + [TestCase("**t**t")] + [TestCase("t**t**")] + [TestCase("**t\tt**")] + + [TestCase(" __t__")] + [TestCase(" __t__t")] + [TestCase(" t__t__")] + [TestCase(" __t t__")] + [TestCase(" __t\tt__")] + [TestCase(" **t**")] + [TestCase(" **t**t")] + [TestCase(" t**t**")] + [TestCase(" **t\tt**")] + + [TestCase("__t__ ")] + [TestCase("__t__t ")] + [TestCase("t__t__ ")] + [TestCase("__t t__ ")] + [TestCase("__t\tt__ ")] + [TestCase("**t** ")] + [TestCase("**t**t ")] + [TestCase("t**t** ")] + [TestCase("**t\tt** ")] + + [TestCase(" __t__ ")] + [TestCase(" __t__t ")] + [TestCase(" t__t__ ")] + [TestCase(" __t t__ ")] + [TestCase(" __t\tt__ ")] + [TestCase(" **t** ")] + [TestCase(" **t** t")] + [TestCase(" t**t** ")] + [TestCase(" **t\tt** ")] + + [TestCase("__t__\t")] + [TestCase("__t__t\t")] + [TestCase("t__t__\t ")] + [TestCase("__t t__\t ")] + [TestCase("__t\tt__\t ")] + [TestCase("**t**\t ")] + [TestCase("**t**t\t ")] + [TestCase("t**t**\t ")] + [TestCase("**t\tt**\t ")] + + [TestCase(" __t__\t ")] + [TestCase(" __t__t\t ")] + [TestCase(" t__t__\t ")] + [TestCase(" __t t__\t ")] + [TestCase(" __t\tt__\t ")] + [TestCase(" **t**\t ")] + [TestCase(" **t**\t t")] + [TestCase(" t**t**\t ")] + [TestCase(" **t\tt**\t ")] + public void Test_StrongEmphasis(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs deleted file mode 100644 index 7c9af3363..000000000 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntities.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; - -namespace Markdig.Tests.RoundtripSpecs.Inlines -{ - /// - /// - /// - /// - [TestFixture] - public class TestHtmlEntities - { - [TestCase(">")] - [TestCase("<")] - [TestCase(" ")] - [TestCase("♥")] - public void Test(string value) - { - RoundTrip(value); - } - } -} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs new file mode 100644 index 000000000..879a98f5f --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs @@ -0,0 +1,53 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + /// + /// + /// + /// + [TestFixture] + public class TestHtmlEntityInline + { + [TestCase(">")] + [TestCase("<")] + [TestCase(" ")] + [TestCase("♥")] + [TestCase("*")] + [TestCase("�")] + [TestCase("Ӓ")] + [TestCase("ಫ")] + + [TestCase(" >")] + [TestCase(" <")] + [TestCase("  ")] + [TestCase(" ♥")] + [TestCase(" *")] + [TestCase(" �")] + [TestCase(" Ӓ")] + [TestCase(" ಫ")] + + [TestCase("> ")] + [TestCase("< ")] + [TestCase("  ")] + [TestCase("♥ ")] + [TestCase("* ")] + [TestCase("� ")] + [TestCase("Ӓ ")] + [TestCase("ಫ ")] + + [TestCase(" > ")] + [TestCase(" < ")] + [TestCase("   ")] + [TestCase(" ♥ ")] + [TestCase(" * ")] + [TestCase(" � ")] + [TestCase(" Ӓ ")] + [TestCase(" ಫ ")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs new file mode 100644 index 000000000..58b0678cc --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs @@ -0,0 +1,15 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestHtmlInline + { + [TestCase(" ಫ ")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs similarity index 94% rename from src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs rename to src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs index cee2aea9d..71e4a1ddb 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImage.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs @@ -4,7 +4,7 @@ namespace Markdig.Tests.RoundtripSpecs.Inlines { [TestFixture] - public class TestImage + public class TestImageInline { [TestCase("![](a)")] [TestCase(" ![](a)")] diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs similarity index 81% rename from src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs rename to src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs index 760b92a3a..50cf89795 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscape.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs @@ -4,7 +4,7 @@ namespace Markdig.Tests.RoundtripSpecs.Inlines { - class TestBackslashEscape + class TestLiteralInline { } } diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs new file mode 100644 index 000000000..3b2774cdb --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; + +namespace Markdig.Tests.RoundtripSpecs.Inlines +{ + [TestFixture] + public class TestNullCharacterInline + { + [TestCase("\0p")] + [TestCase("p\0")] + [TestCase("p\0p")] + [TestCase("p\0\0p")] // I promise you, this was not intentional + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 81f01a526..0d413f869 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -10,4 +10,20 @@ Quoteblocks may have different syntactical characters applied per line. That is, ## Lists - beforewhitespace on list item -- \ No newline at end of file + +# TODO +In order: +- `p\n p`: affects many tests +- `\r\n` and `\r` support +- `\0` +- support link parsing +- support LinkReferenceDefinition +- fix broken pre-existing tests +- fix `TODO: RTP: ` +- generate spec examples as tests for roundtrip +- run pull request feedback +- extract MarkdownRenderer +- deduplicate MarkdownRenderer and NormalizeRenderer code +- support extensions +- write tree comparison tests? +- write tree visualization tool? diff --git a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs index b55c3b099..652156a94 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs @@ -24,7 +24,7 @@ public void Test(string value) [TestCase("\n\n# h\n\np\n\n")] [TestCase("\n\n# h\np\n\n")] [TestCase("\n\n# h\np\n\n")] - public void TestParagrph(string value) + public void TestParagraph(string value) { RoundTrip(value); } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index 0fe7d799f..93ff47251 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -3,6 +3,7 @@ namespace Markdig.Tests.RoundtripSpecs { + // TODO: RTP: test info strings [TestFixture] public class TestFencedCodeBlock { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs index ee9a36c21..8fbff4fa5 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs @@ -12,7 +12,7 @@ public class TestHtmlBlock [TestCase("
\n\n# h")] [TestCase("p\n\n
\n")] [TestCase("
\n\n# h")] - public void TestHtml(string value) + public void Test(string value) { RoundTrip(value); } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index 9ab46f12e..24c52b880 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -10,8 +10,8 @@ public class TestIndentedCodeBlock // l = line [TestCase(" l")] [TestCase(" l")] - //[TestCase("\tl")] - //[TestCase("\tl1\n l1")] + [TestCase("\tl")] + [TestCase("\tl1\n l1")] [TestCase("\n l")] [TestCase("\n\n l")] diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs new file mode 100644 index 000000000..963a5831f --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Markdig.Tests.RoundtripSpecs +{ + class TestLinkReferenceDefinition + { + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index 32f5eb80d..f589a0c26 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -10,9 +10,101 @@ public class TestOrderedList [TestCase("1. i")] [TestCase("1. i ")] [TestCase("1. i ")] + [TestCase("1. i ")] + + [TestCase(" 1. i")] + [TestCase(" 1. i")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + + [TestCase(" 1. i")] + [TestCase(" 1. i")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + + [TestCase(" 1. i")] + [TestCase(" 1. i")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + [TestCase(" 1. i ")] + + [TestCase("1. i\n")] + [TestCase("1. i\n")] + [TestCase("1. i \n")] + [TestCase("1. i \n")] + [TestCase("1. i \n")] + + [TestCase(" 1. i\n")] + [TestCase(" 1. i\n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + + [TestCase(" 1. i\n")] + [TestCase(" 1. i\n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + + [TestCase(" 1. i\n")] + [TestCase(" 1. i\n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + [TestCase(" 1. i \n")] + + [TestCase("1. i\n2. j")] + [TestCase("1. i\n2. j")] + [TestCase("1. i \n2. j")] + [TestCase("1. i \n2. j")] + [TestCase("1. i \n2. j")] + + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i\n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + [TestCase(" 1. i \n2. j")] + + [TestCase("1. i\n2. j\n")] + [TestCase("1. i\n2. j\n")] + [TestCase("1. i \n2. j\n")] + [TestCase("1. i \n2. j\n")] + [TestCase("1. i \n2. j\n")] + + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i\n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + [TestCase(" 1. i \n2. j\n")] + + [TestCase("1. i\n2. j\n3. k")] + [TestCase("1. i\n2. j\n3. k\n")] - [TestCase("1. i1\n2. i2")] - [TestCase("1. i1\n2. i2\n a. i2.1")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index af5f031a4..4701b138c 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -96,6 +96,12 @@ public class TestParagraph [TestCase("\np \n\n p ")] [TestCase("\n p \n\n p ")] + [TestCase("p p")] + [TestCase("p\tp")] + [TestCase("p \tp")] + [TestCase("p \t p")] + [TestCase("p \tp")] + // special cases [TestCase(" p \n\n\n\n p \n\n")] [TestCase("\n\np")] diff --git a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs index 03ece89fb..b4108a164 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs @@ -6,8 +6,21 @@ namespace Markdig.Tests.RoundtripSpecs [TestFixture] public class TestSetextHeading { - [TestCase("h1===\n")] - [TestCase("h2---\n")] + [TestCase("h1\n===")] //3 + [TestCase("h1\n ===")] //3 + [TestCase("h1\n ===")] //3 + [TestCase("h1\n ===")] //3 + [TestCase("h1\n=== ")] //3 + [TestCase("h1 \n===")] //3 + [TestCase("h1\\\n===")] //3 + [TestCase("h1\n === ")] //3 + [TestCase("h1\nh1 l2\n===")] //3 + [TestCase("h1\n====")] // 4 + [TestCase("h1\n ====")] // 4 + [TestCase("h1\n==== ")] // 4 + [TestCase("h1\n ==== ")] // 4 + [TestCase("h1\n===\nh1\n===")] //3 + [TestCase("\\>h1\n===")] //3 public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index e330a4a64..b0f21c261 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -69,16 +69,22 @@ public void TestThematicBreak(string value) RoundTrip(value); } - [TestCase("- c")] - [TestCase("- c\n c")] - [TestCase(" - c\n c")] + [TestCase("- c")] // 5 + [TestCase("- c\n c")] // 5, 6 + [TestCase(" - c\n c")] // 5, 6 + [TestCase(" - c\n c")] // 5, 7 + [TestCase("- c\n c")] // 6, 6 + [TestCase(" - c\n c")] // 6, 6 + [TestCase(" - c\n c")] // 6, 7 public void TestIndentedCodeBlock(string value) { RoundTrip(value); } [TestCase("- ```a```")] - [TestCase("- i1\n - i1.1\n ```\n code\n ```")] + [TestCase("- i1\n - i1.1\n ```\n c\n ```")] + [TestCase("- i1\n - i1.1\n ```\nc\n```")] + [TestCase("- i1\n - i1.1\n ```\nc\n```\n")] public void TestFencedCodeBlock(string value) { RoundTrip(value); diff --git a/src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs b/src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs new file mode 100644 index 000000000..a552712c4 --- /dev/null +++ b/src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Markdig.Renderers.Normalize +{ + class SetextHeadingRenderer + { + } +} From fa3b67342dc7943a1949b2f31004066e3219b9bc Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 10 Oct 2020 17:10:18 +0200 Subject: [PATCH 035/120] add some tests, update todolist --- .../RoundtripSpecs/Inlines/TestHtmlInline.cs | 14 +++++++++++++- .../Inlines/TestNullCharacterInline.cs | 1 + src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 7 ++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs index 58b0678cc..65d84ace3 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs @@ -6,7 +6,19 @@ namespace Markdig.Tests.RoundtripSpecs.Inlines [TestFixture] public class TestHtmlInline { - [TestCase(" ಫ ")] + [TestCase("f")] + [TestCase(" f")] + [TestCase("f ")] + [TestCase(" f ")] + [TestCase("p")] + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase("\t")] + [TestCase(" \t")] + [TestCase("\t ")] + [TestCase(" \t ")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs index 3b2774cdb..f78c66cb3 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -6,6 +6,7 @@ namespace Markdig.Tests.RoundtripSpecs.Inlines [TestFixture] public class TestNullCharacterInline { + [TestCase("\0")] [TestCase("\0p")] [TestCase("p\0")] [TestCase("p\0p")] diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 0d413f869..080caa851 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -15,15 +15,20 @@ Quoteblocks may have different syntactical characters applied per line. That is, In order: - `p\n p`: affects many tests - `\r\n` and `\r` support -- `\0` - support link parsing - support LinkReferenceDefinition - fix broken pre-existing tests - fix `TODO: RTP: ` - generate spec examples as tests for roundtrip - run pull request feedback +- introduce feature flag - extract MarkdownRenderer - deduplicate MarkdownRenderer and NormalizeRenderer code +- `\0` - support extensions +- review complete PR and follow conventions +- run perf test +- document how trivia are handled generically and specifically +- create todo list with perf optimization focus points - write tree comparison tests? - write tree visualization tool? From d15edb79fa3cf70d7126d6ec1223f602243f1eaa Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 10 Oct 2020 17:25:22 +0200 Subject: [PATCH 036/120] fix broken whitespace starting on newlines within a paragraph --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 1 + .../RoundtripSpecs/TestQuoteBlock.cs | 19 ++++++++++++++++++- src/Markdig/Parsers/BlockProcessor.cs | 14 ++++++++++++++ src/Markdig/Parsers/ListBlockParser.cs | 5 ++--- src/Markdig/Parsers/ParagraphBlockParser.cs | 1 - 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 080caa851..0da2d9f1d 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -15,6 +15,7 @@ Quoteblocks may have different syntactical characters applied per line. That is, In order: - `p\n p`: affects many tests - `\r\n` and `\r` support +- support SetextHeading - support link parsing - support LinkReferenceDefinition - fix broken pre-existing tests diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 4040cb726..2c786e386 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -168,7 +168,6 @@ public class TestQuoteBlock [TestCase(">q\n>\n>\n>q")] [TestCase(">q\n>\n>\n>\n>q")] [TestCase(">q\n>\n>q\n>\n>q")] - [TestCase("> **q**\n>p\n")] [TestCase("p\n\n> **q**\n>p\n")] [TestCase("> q\np\n> q")] // lazy @@ -176,6 +175,9 @@ public class TestQuoteBlock [TestCase(">>q")] [TestCase(" > > q")] + + [TestCase("> **q**\n>p\n")] + [TestCase("> **q**")] public void Test(string value) { RoundTrip(value); @@ -226,5 +228,20 @@ public void TestUnorderedList(string value) { RoundTrip(value); } + + [TestCase("> *q*\n>p\n")] + [TestCase("> *q*")] + public void TestEmphasis(string value) + { + RoundTrip(value); + } + + [TestCase("> **q**\n>p\n")] + [TestCase("> **q**")] + public void TestStrongEmphasis(string value) + { + RoundTrip(value); + } + } } diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 872f5f4c6..35dbd5623 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -20,6 +20,7 @@ public class BlockProcessor private int currentStackIndex; private readonly BlockParserStateCache parserStateCache; private int originalLineStart = 0; + private bool trackTrivia = true; private BlockProcessor(BlockProcessor root) { @@ -669,6 +670,10 @@ private void TryContinueBlocks() ContinueProcessingLine = false; if (!result.IsDiscard()) { + if (trackTrivia) + { + //UnwindAllIndents(); + } leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); } } @@ -808,6 +813,10 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (!result.IsDiscard()) { // TODO: RTP: pass line with whitespace + if (trackTrivia) + { + UnwindAllIndents(); + } paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); } // TODO: RTP: delegate this to container parser classes @@ -867,6 +876,11 @@ private void ProcessNewBlocks(BlockState result, bool allowClosing) { if (!result.IsDiscard()) { + bool isParagraph = block is ParagraphBlock; + if (trackTrivia && isParagraph) + { + UnwindAllIndents(); + } leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); } diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 6abac5b1f..d7d0a2173 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -208,8 +208,8 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) return BlockState.None; } var bulletLength = 1; // TODO: RTP: fix for ordered - var whitespaceBefore = state.PopBeforeWhitespace(state.Start - 1 - bulletLength); // -1: - int whitespaceAfterStart = state.Start; + var whitespaceBefore = state.PopBeforeWhitespace(state.Start - bulletLength - 1); + state.WhitespaceStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; @@ -271,7 +271,6 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) ColumnWidth = columnWidth, Order = order, BeforeWhitespace = whitespaceBefore, - AfterWhitespace = state.PopBeforeWhitespace(whitespaceAfterStart, state.Start - 1), Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), }; diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 713c0a75c..57a31d3a8 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -28,7 +28,6 @@ public override BlockState TryOpen(BlockProcessor processor) { Column = processor.Column, Span = new SourceSpan(processor.Line.Start, processor.Line.End), - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), LinesBefore = processor.UseLinesBefore() }); return BlockState.Continue; From 6ba3c3d6830c82c3890c470075e0289b835f5a53 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 11 Oct 2020 14:00:09 +0200 Subject: [PATCH 037/120] implement newline --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 2 +- .../RoundtripSpecs/TestAtxHeading.cs | 19 ++++ .../RoundtripSpecs/TestFencedCodeBlock.cs | 48 ++++++++++ .../RoundtripSpecs/TestIndentedCodeBlock.cs | 23 +++++ .../RoundtripSpecs/TestOrderedList.cs | 50 +++++++++++ .../RoundtripSpecs/TestParagraph.cs | 89 +++++++++++++++++++ .../RoundtripSpecs/TestQuoteBlock.cs | 23 +++++ .../RoundtripSpecs/TestSetextHeading.cs | 20 +++++ .../RoundtripSpecs/TestThematicBreak.cs | 24 ++++- .../RoundtripSpecs/TestUnorderedList.cs | 84 +++++++++++++++++ .../CustomContainers/CustomContainer.cs | 2 + src/Markdig/Helpers/LineReader.cs | 18 +++- src/Markdig/Helpers/StringLine.cs | 8 +- src/Markdig/Helpers/StringLineGroup.cs | 11 ++- src/Markdig/Helpers/StringSlice.cs | 59 ++++++++++++ src/Markdig/Parsers/FencedBlockParserBase.cs | 7 +- src/Markdig/Parsers/FencedCodeBlockParser.cs | 3 +- src/Markdig/Parsers/HeadingBlockParser.cs | 3 +- src/Markdig/Parsers/HtmlBlockParser.cs | 2 + .../Parsers/IndentedCodeBlockParser.cs | 2 + src/Markdig/Parsers/InlineProcessor.cs | 18 +++- .../Parsers/Inlines/LineBreakInlineParser.cs | 20 ++++- .../Parsers/Inlines/LiteralInlineParser.cs | 2 +- src/Markdig/Parsers/ListBlockParser.cs | 2 + src/Markdig/Parsers/MarkdownParser.cs | 1 - src/Markdig/Parsers/ParagraphBlockParser.cs | 26 ++++-- src/Markdig/Parsers/QuoteBlockParser.cs | 5 +- src/Markdig/Parsers/ThematicBreakParser.cs | 3 +- .../Renderers/Normalize/CodeBlockRenderer.cs | 12 +-- .../Renderers/Normalize/HeadingRenderer.cs | 35 ++++++-- .../Renderers/Normalize/HtmlBlockRenderer.cs | 2 +- .../Inlines/LineBreakInlineRenderer.cs | 2 +- .../Renderers/Normalize/ListRenderer.cs | 11 --- .../Renderers/Normalize/NormalizeRenderer.cs | 27 ++---- .../Renderers/Normalize/ParagraphRenderer.cs | 1 - .../Renderers/Normalize/QuoteBlockRenderer.cs | 3 +- .../Normalize/ThematicBreakRenderer.cs | 2 +- src/Markdig/Renderers/TextRendererBase.cs | 14 +++ src/Markdig/Syntax/Block.cs | 5 ++ src/Markdig/Syntax/FencedCodeBlock.cs | 2 + src/Markdig/Syntax/HeadingBlock.cs | 7 ++ src/Markdig/Syntax/IFencedBlock.cs | 5 ++ src/Markdig/Syntax/Inlines/LineBreakInline.cs | 4 + src/Markdig/Syntax/LeafBlock.cs | 3 +- src/Markdig/Syntax/QuoteBlock.cs | 2 + 45 files changed, 624 insertions(+), 87 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 0da2d9f1d..1c250e9d9 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -13,7 +13,7 @@ Quoteblocks may have different syntactical characters applied per line. That is, # TODO In order: -- `p\n p`: affects many tests +- ~~`p\n p`: affects many tests~~ - `\r\n` and `\r` support - support SetextHeading - support link parsing diff --git a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs index 652156a94..4ad665d08 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs @@ -28,5 +28,24 @@ public void TestParagraph(string value) { RoundTrip(value); } + + [TestCase("\n# h")] + [TestCase("\n# h\n")] + [TestCase("\n# h\r")] + [TestCase("\n# h\r\n")] + + [TestCase("\r# h")] + [TestCase("\r# h\n")] + [TestCase("\r# h\r")] + [TestCase("\r# h\r\n")] + + [TestCase("\r\n# h")] + [TestCase("\r\n# h\n")] + [TestCase("\r\n# h\r")] + [TestCase("\r\n# h\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index 93ff47251..4f4be5e6c 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -21,9 +21,57 @@ public class TestFencedCodeBlock [TestCase(" ```\nc\n````")] [TestCase("```\nc\n````")] [TestCase("p\n\n```\nc\n```")] + + [TestCase("```\n c\n```")] + [TestCase("```\nc \n```")] + [TestCase("```\n c \n```")] public void Test(string value) { RoundTrip(value); } + + [TestCase("```\n c \n```")] + [TestCase("```\n c \r```")] + [TestCase("```\n c \r\n```")] + [TestCase("```\r c \n```")] + [TestCase("```\r c \r```")] + [TestCase("```\r c \r\n```")] + [TestCase("```\r\n c \n```")] + [TestCase("```\r\n c \r```")] + [TestCase("```\r\n c \r\n```")] + + [TestCase("```\n c \n```\n")] + [TestCase("```\n c \r```\n")] + [TestCase("```\n c \r\n```\n")] + [TestCase("```\r c \n```\n")] + [TestCase("```\r c \r```\n")] + [TestCase("```\r c \r\n```\n")] + [TestCase("```\r\n c \n```\n")] + [TestCase("```\r\n c \r```\n")] + [TestCase("```\r\n c \r\n```\n")] + + [TestCase("```\n c \n```\r")] + [TestCase("```\n c \r```\r")] + [TestCase("```\n c \r\n```\r")] + [TestCase("```\r c \n```\r")] + [TestCase("```\r c \r```\r")] + [TestCase("```\r c \r\n```\r")] + [TestCase("```\r\n c \n```\r")] + [TestCase("```\r\n c \r```\r")] + [TestCase("```\r\n c \r\n```\r")] + + [TestCase("```\n c \n```\r\n")] + [TestCase("```\n c \r```\r\n")] + [TestCase("```\n c \r\n```\r\n")] + [TestCase("```\r c \n```\r\n")] + [TestCase("```\r c \r```\r\n")] + [TestCase("```\r c \r\n```\r\n")] + [TestCase("```\r\n c \n```\r\n")] + [TestCase("```\r\n c \r```\r\n")] + [TestCase("```\r\n c \r\n```\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index 24c52b880..23c7c1bde 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -38,5 +38,28 @@ public void Test(string value) { RoundTrip(value); } + + [TestCase(" l\n")] + [TestCase(" l\r")] + [TestCase(" l\r\n")] + + [TestCase(" l\n l")] + [TestCase(" l\n l\n")] + [TestCase(" l\n l\r")] + [TestCase(" l\n l\r\n")] + + [TestCase(" l\r l")] + [TestCase(" l\r l\n")] + [TestCase(" l\r l\r")] + [TestCase(" l\r l\r\n")] + + [TestCase(" l\r\n l")] + [TestCase(" l\r\n l\n")] + [TestCase(" l\r\n l\r")] + [TestCase(" l\r\n l\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index f589a0c26..247d08888 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -109,5 +109,55 @@ public void Test(string value) { RoundTrip(value); } + + [TestCase("\n1. i")] + [TestCase("\r1. i")] + [TestCase("\r\n1. i")] + + [TestCase("\n1. i\n")] + [TestCase("\r1. i\n")] + [TestCase("\r\n1. i\n")] + + [TestCase("\n1. i\r")] + [TestCase("\r1. i\r")] + [TestCase("\r\n1. i\r")] + + [TestCase("\n1. i\r\n")] + [TestCase("\r1. i\r\n")] + [TestCase("\r\n1. i\r\n")] + + [TestCase("1. i\n2. i")] + [TestCase("\n1. i\n2. i")] + [TestCase("\r1. i\n2. i")] + [TestCase("\r\n1. i\n2. i")] + + [TestCase("1. i\r2. i")] + [TestCase("\n1. i\r2. i")] + [TestCase("\r1. i\r2. i")] + [TestCase("\r\n1. i\r2. i")] + + [TestCase("1. i\r\n2. i")] + [TestCase("\n1. i\r\n2. i")] + [TestCase("\r1. i\r\n2. i")] + [TestCase("\r\n1. i\r\n2. i")] + + [TestCase("1. i\n2. i\n")] + [TestCase("\n1. i\n2. i\n")] + [TestCase("\r1. i\n2. i\n")] + [TestCase("\r\n1. i\n2. i\n")] + + [TestCase("1. i\r2. i\r")] + [TestCase("\n1. i\r2. i\r")] + [TestCase("\r1. i\r2. i\r")] + [TestCase("\r\n1. i\r2. i\r")] + + [TestCase("1. i\r\n2. i\r\n")] + [TestCase("\n1. i\r\n2. i\r\n")] + [TestCase("\r1. i\r\n2. i\r\n")] + [TestCase("\r\n1. i\r\n2. i\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index 4701b138c..b46f19f08 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -113,5 +113,94 @@ public void Test(string value) { RoundTrip(value); } + + + //[TestCase("\n")] + //[TestCase("\r\n")] + //[TestCase("\r")] + + [TestCase("p\n")] + [TestCase("p\r")] + [TestCase("p\r\n")] + + [TestCase("p\np")] + [TestCase("p\rp")] + [TestCase("p\r\np")] + + [TestCase("p\np\n")] + [TestCase("p\rp\n")] + [TestCase("p\r\np\n")] + + [TestCase("p\np\r")] + [TestCase("p\rp\r")] + [TestCase("p\r\np\r")] + + [TestCase("p\np\r\n")] + [TestCase("p\rp\r\n")] + [TestCase("p\r\np\r\n")] + + [TestCase("\np\n")] + [TestCase("\np\r")] + [TestCase("\np\r\n")] + + [TestCase("\np\np")] + [TestCase("\np\rp")] + [TestCase("\np\r\np")] + + [TestCase("\np\np\n")] + [TestCase("\np\rp\n")] + [TestCase("\np\r\np\n")] + + [TestCase("\np\np\r")] + [TestCase("\np\rp\r")] + [TestCase("\np\r\np\r")] + + [TestCase("\np\np\r\n")] + [TestCase("\np\rp\r\n")] + [TestCase("\np\r\np\r\n")] + + [TestCase("\rp\n")] + [TestCase("\rp\r")] + [TestCase("\rp\r\n")] + + [TestCase("\rp\np")] + [TestCase("\rp\rp")] + [TestCase("\rp\r\np")] + + [TestCase("\rp\np\n")] + [TestCase("\rp\rp\n")] + [TestCase("\rp\r\np\n")] + + [TestCase("\rp\np\r")] + [TestCase("\rp\rp\r")] + [TestCase("\rp\r\np\r")] + + [TestCase("\rp\np\r\n")] + [TestCase("\rp\rp\r\n")] + [TestCase("\rp\r\np\r\n")] + + [TestCase("\r\np\n")] + [TestCase("\r\np\r")] + [TestCase("\r\np\r\n")] + + [TestCase("\r\np\np")] + [TestCase("\r\np\rp")] + [TestCase("\r\np\r\np")] + + [TestCase("\r\np\np\n")] + [TestCase("\r\np\rp\n")] + [TestCase("\r\np\r\np\n")] + + [TestCase("\r\np\np\r")] + [TestCase("\r\np\rp\r")] + [TestCase("\r\np\r\np\r")] + + [TestCase("\r\np\np\r\n")] + [TestCase("\r\np\rp\r\n")] + [TestCase("\r\np\r\np\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 2c786e386..9534cc018 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -243,5 +243,28 @@ public void TestStrongEmphasis(string value) RoundTrip(value); } + [TestCase(">p\n")] + [TestCase(">p\r")] + [TestCase(">p\r\n")] + + [TestCase(">p\n>p")] + [TestCase(">p\r>p")] + [TestCase(">p\r\n>p")] + + [TestCase(">p\n>p\n")] + [TestCase(">p\r>p\n")] + [TestCase(">p\r\n>p\n")] + + [TestCase(">p\n>p\r")] + [TestCase(">p\r>p\r")] + [TestCase(">p\r\n>p\r")] + + [TestCase(">p\n>p\r\n")] + [TestCase(">p\r>p\r\n")] + [TestCase(">p\r\n>p\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs index b4108a164..17ed72726 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs @@ -25,5 +25,25 @@ public void Test(string value) { RoundTrip(value); } + + [TestCase("h1\r===")] + [TestCase("h1\n===")] + [TestCase("h1\r\n===")] + + [TestCase("h1\r===\r")] + [TestCase("h1\n===\r")] + [TestCase("h1\r\n===\r")] + + [TestCase("h1\r===\n")] + [TestCase("h1\n===\n")] + [TestCase("h1\r\n===\n")] + + [TestCase("h1\r===\r\n")] + [TestCase("h1\n===\r\n")] + [TestCase("h1\r\n===\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs index 16e283bab..2e9794f23 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs @@ -23,11 +23,31 @@ public class TestThematicBreak [TestCase("---\np")] [TestCase("---\n\np")] [TestCase("---\n# h")] - //[TestCase("p\n\n---")] - /// Note: "p\n---" is parsed as setext heading + [TestCase("p\n\n---")] + // Note: "p\n---" is parsed as setext heading public void Test(string value) { RoundTrip(value); } + + [TestCase("\n---")] + [TestCase("\r---")] + [TestCase("\r\n---")] + + [TestCase("\n---\n")] + [TestCase("\r---\n")] + [TestCase("\r\n---\n")] + + [TestCase("\n---\r")] + [TestCase("\r---\r")] + [TestCase("\r\n---\r")] + + [TestCase("\n---\r\n")] + [TestCase("\r---\r\n")] + [TestCase("\r\n---\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index b0f21c261..1bb0ab000 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -89,5 +89,89 @@ public void TestFencedCodeBlock(string value) { RoundTrip(value); } + + [TestCase("\n- i")] + [TestCase("\r- i")] + [TestCase("\r\n- i")] + + [TestCase("\n- i\n")] + [TestCase("\r- i\n")] + [TestCase("\r\n- i\n")] + + [TestCase("\n- i\r")] + [TestCase("\r- i\r")] + [TestCase("\r\n- i\r")] + + [TestCase("\n- i\r\n")] + [TestCase("\r- i\r\n")] + [TestCase("\r\n- i\r\n")] + + [TestCase("- i\n- j")] + [TestCase("- i\r- j")] + [TestCase("- i\r\n- j")] + + [TestCase("\n- i\n- j")] + [TestCase("\n- i\r- j")] + [TestCase("\n- i\r\n- j")] + + [TestCase("\r- i\n- j")] + [TestCase("\r- i\r- j")] + [TestCase("\r- i\r\n- j")] + + [TestCase("\r\n- i\n- j")] + [TestCase("\r\n- i\r- j")] + [TestCase("\r\n- i\r\n- j")] + + [TestCase("- i\n- j\n")] + [TestCase("- i\r- j\n")] + [TestCase("- i\r\n- j\n")] + + [TestCase("\n- i\n- j\n")] + [TestCase("\n- i\r- j\n")] + [TestCase("\n- i\r\n- j\n")] + + [TestCase("\r- i\n- j\n")] + [TestCase("\r- i\r- j\n")] + [TestCase("\r- i\r\n- j\n")] + + [TestCase("\r\n- i\n- j\n")] + [TestCase("\r\n- i\r- j\n")] + [TestCase("\r\n- i\r\n- j\n")] + + [TestCase("- i\n- j\r")] + [TestCase("- i\r- j\r")] + [TestCase("- i\r\n- j\r")] + + [TestCase("\n- i\n- j\r")] + [TestCase("\n- i\r- j\r")] + [TestCase("\n- i\r\n- j\r")] + + [TestCase("\r- i\n- j\r")] + [TestCase("\r- i\r- j\r")] + [TestCase("\r- i\r\n- j\r")] + + [TestCase("\r\n- i\n- j\r")] + [TestCase("\r\n- i\r- j\r")] + [TestCase("\r\n- i\r\n- j\r")] + + [TestCase("- i\n- j\r\n")] + [TestCase("- i\r- j\r\n")] + [TestCase("- i\r\n- j\r\n")] + + [TestCase("\n- i\n- j\r\n")] + [TestCase("\n- i\r- j\r\n")] + [TestCase("\n- i\r\n- j\r\n")] + + [TestCase("\r- i\n- j\r\n")] + [TestCase("\r- i\r- j\r\n")] + [TestCase("\r- i\r\n- j\r\n")] + + [TestCase("\r\n- i\n- j\r\n")] + [TestCase("\r\n- i\r- j\r\n")] + [TestCase("\r\n- i\r\n- j\r\n")] + public void TestNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index a3eb5aab9..a5f353bde 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; using Markdig.Syntax; @@ -37,5 +38,6 @@ public CustomContainer(BlockParser parser) : base(parser) public int ClosingFencedCharCount { get; set; } + public Newline InfoNewline { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index 1906e0a73..20ebfd5ab 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -50,13 +50,25 @@ public StringSlice ReadLine() for (int i = sourcePosition; i < text.Length; i++) { char c = text[i]; - if (c == '\r' || c == '\n') + if (c == '\r') { - var slice = new StringSlice(text, sourcePosition, i - 1); - + int length = 1; + var newline = Newline.CarriageReturn; if (c == '\r' && (uint)(i + 1) < (uint)text.Length && text[i + 1] == '\n') + { i++; + length = 2; + newline = Newline.CarriageReturnLineFeed; + } + + var slice = new StringSlice(text, sourcePosition, i - length, newline); + SourcePosition = i + 1; + return slice; + } + if (c == '\n') + { + var slice = new StringSlice(text, sourcePosition, i - 1, Newline.LineFeed); SourcePosition = i + 1; return slice; } diff --git a/src/Markdig/Helpers/StringLine.cs b/src/Markdig/Helpers/StringLine.cs index a17a317cb..67c9303be 100644 --- a/src/Markdig/Helpers/StringLine.cs +++ b/src/Markdig/Helpers/StringLine.cs @@ -25,12 +25,13 @@ public StringLine(ref StringSlice slice) : this() /// The line. /// The column. /// The position. - public StringLine(StringSlice slice, int line, int column, int position) + public StringLine(StringSlice slice, int line, int column, int position, Newline newline) { Slice = slice; Line = line; Column = column; Position = position; + Newline = newline; } /// @@ -40,12 +41,13 @@ public StringLine(StringSlice slice, int line, int column, int position) /// The line. /// The column. /// The position. - public StringLine(ref StringSlice slice, int line, int column, int position) + public StringLine(ref StringSlice slice, int line, int column, int position, Newline newline) { Slice = slice; Line = line; Column = column; Position = position; + Newline = newline; } /// @@ -68,6 +70,8 @@ public StringLine(ref StringSlice slice, int line, int column, int position) /// public int Column; + public Newline Newline; + /// /// Performs an implicit conversion from to . /// diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index be779d2f4..8afab5e1f 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -121,14 +121,15 @@ public readonly StringSlice ToSlice(List lineOffsets = null) // Optimization case for a single line. if (Count == 1) { - lineOffsets?.Add(new LineOffset(Lines[0].Position, Lines[0].Column, Lines[0].Slice.Start - Lines[0].Position, Lines[0].Slice.Start, Lines[0].Slice.End + 1)); + var l = Lines[0]; + lineOffsets?.Add(new LineOffset(l.Position, l.Column, l.Slice.Start - l.Position, l.Slice.Start, l.Slice.End + 1)); return Lines[0]; } // Optimization case when no lines if (Count == 0) { - return new StringSlice(string.Empty); + return StringSlice.Empty; } if (lineOffsets != null && lineOffsets.Capacity < lineOffsets.Count + Count) @@ -139,18 +140,20 @@ public readonly StringSlice ToSlice(List lineOffsets = null) // Else use a builder var builder = StringBuilderCache.Local(); int previousStartOfLine = 0; + var newline = Newline.None; for (int i = 0; i < Count; i++) { if (i > 0) { - builder.Append('\n'); - previousStartOfLine = builder.Length; + builder.Append(newline); + previousStartOfLine = builder.Length; } ref var line = ref Lines[i]; if (!line.Slice.IsEmpty) { builder.Append(line.Slice.Text, line.Slice.Start, line.Slice.Length); } + newline = line.Newline; lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, previousStartOfLine, builder.Length)); } diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 786046d01..49c15d7d7 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -7,6 +7,43 @@ namespace Markdig.Helpers { + /// + /// Wrap newline so we have type-safety and static accessibility + /// + public struct Newline + { + private readonly bool carriageReturn; + private readonly bool lineFeed; + + private Newline(bool carriageReturn, bool lineFeed) + { + this.carriageReturn = carriageReturn; + this.lineFeed = lineFeed; + } + + public static Newline None = new Newline(false, false); + public static Newline CarriageReturn = new Newline(true, false); + public static Newline LineFeed = new Newline(false, true); + public static Newline CarriageReturnLineFeed = new Newline(true, true); + + public static implicit operator string (Newline newline) + { + if (newline.carriageReturn && newline.lineFeed) + { + return "\r\n"; + } + if (newline.lineFeed) + { + return "\n"; + } + if (newline.carriageReturn) + { + return "\r"; + } + return string.Empty; + } + } + /// /// A lightweight struct that represents a slice of a string. /// @@ -27,6 +64,7 @@ public StringSlice(string text) Text = text; Start = 0; End = (Text?.Length ?? 0) - 1; + Newline = Newline.None; } /// @@ -44,6 +82,25 @@ public StringSlice(string text, int start, int end) Text = text; Start = start; End = end; + Newline = Newline.None; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The text. + /// The start. + /// The end. + /// + public StringSlice(string text, int start, int end, Newline newline) + { + if (text is null) + ThrowHelper.ArgumentNullException_text(); + + Text = text; + Start = start; + End = end; + Newline = newline; } /// @@ -66,6 +123,8 @@ public StringSlice(string text, int start, int end) /// public readonly int Length => End - Start + 1; + public Newline Newline; + /// /// Gets the current character. /// diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 572d0193d..d78bb090c 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -2,7 +2,6 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System; using Markdig.Helpers; using Markdig.Renderers.Html; using Markdig.Syntax; @@ -166,6 +165,7 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice fenced.WhitespaceAfterInfo = afterInfo; fenced.Arguments = HtmlHelper.Unescape(arg); fenced.WhitespaceAfterArguments = afterArg; + fenced.InfoNewline = line.Newline; return true; } @@ -319,7 +319,10 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) { block.UpdateSpanEnd(line.Start - 1); - (block as IFencedBlock).ClosingFencedCharCount = closingCount; + + var fencedBlock = block as IFencedBlock; + fencedBlock.ClosingFencedCharCount = closingCount; + fencedBlock.Newline = processor.Line.Newline; // Don't keep the last line return BlockState.BreakDiscard; diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index 376409037..d215e29d9 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -31,7 +31,8 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) { IndentCount = processor.Indent, LinesBefore = processor.UseLinesBefore(), - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1) + BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + Newline = processor.Line.Newline, }; } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 70fde42d7..9143abe3a 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -80,7 +80,8 @@ public override BlockState TryOpen(BlockProcessor processor) Column = column, Span = { Start = sourcePosition }, BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), - LinesBefore = processor.UseLinesBefore() + LinesBefore = processor.UseLinesBefore(), + Newline = processor.Line.Newline, }; processor.NewBlocks.Push(headingBlock); processor.GoToColumn(column + leadingCount + 1); diff --git a/src/Markdig/Parsers/HtmlBlockParser.cs b/src/Markdig/Parsers/HtmlBlockParser.cs index 081b9808c..97d490842 100644 --- a/src/Markdig/Parsers/HtmlBlockParser.cs +++ b/src/Markdig/Parsers/HtmlBlockParser.cs @@ -263,6 +263,7 @@ private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock) if (result != BlockState.BreakDiscard) { htmlBlock.Span.End = line.End; + htmlBlock.Newline = state.Line.Newline; } return result; @@ -277,6 +278,7 @@ private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int // By default, setup to the end of line Span = new SourceSpan(startPosition, startPosition + state.Line.End), LinesBefore = state.UseLinesBefore(), + Newline = state.Line.Newline, }); return BlockState.Continue; } diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index fe5c9b1b1..1bd3007ef 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -37,6 +37,7 @@ public override BlockState TryOpen(BlockProcessor processor) Column = processor.Column, Span = new SourceSpan(processor.Start, processor.Line.End), LinesBefore = processor.UseLinesBefore(), + Newline = processor.Line.Newline, }; var codeBlockLine = new CodeBlockLine { @@ -97,6 +98,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) }; cb.CodeBlockLines ??= new List(); cb.CodeBlockLines.Add(codeBlockLine); + cb.Newline = processor.Line.Newline; // ensure block newline is last newline } return BlockState.Continue; diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 14dd2d33a..d71bfa969 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -48,6 +48,7 @@ public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool lineOffsets = new List(); ParserStates = new object[Parsers.Count]; LiteralInlineParser = new LiteralInlineParser(); + LineBreakInlineParser = new LineBreakInlineParser(); } /// @@ -110,6 +111,8 @@ public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool /// public LiteralInlineParser LiteralInlineParser { get; } + public LineBreakInlineParser LineBreakInlineParser { get; } + public int GetSourcePosition(int sliceOffset) { @@ -185,6 +188,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) lineOffsets.Clear(); var text = leafBlock.Lines.ToSlice(lineOffsets); leafBlock.Lines.Release(); + ContainerInline container = null; int previousStart = -1; @@ -236,7 +240,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) if (nextInline.Parent == null) { // Get deepest container - var container = FindLastContainer(); + container = FindLastContainer(); if (!ReferenceEquals(container, nextInline)) { container.AppendChild(nextInline); @@ -256,7 +260,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) else { // Get deepest container - var container = FindLastContainer(); + container = FindLastContainer(); Inline = container.LastChild is LeafInline ? container.LastChild : container; if (Inline == Root) @@ -272,6 +276,16 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) //} } + if (leafBlock is HeadingBlock heading && heading.IsSetext) + { + // TODO: RTP: delegate to block? + } + else + { + var newline = leafBlock.Newline; + container.AppendChild(new LineBreakInline { Newline = newline }); + } + Inline = null; //if (DebugLog != null) //{ diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index 4be7d92d9..2c713b188 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -19,7 +19,7 @@ public class LineBreakInlineParser : InlineParser /// public LineBreakInlineParser() { - OpeningCharacters = new[] {'\n'}; + OpeningCharacters = new[] {'\n', '\r'}; } /// @@ -37,14 +37,28 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var startPosition = slice.Start; var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace(); - slice.NextChar(); // Skip \n + var newline = Newline.LineFeed; + if (slice.CurrentChar == '\r') + { + if (slice.PeekChar() == '\n') + { + newline = Newline.CarriageReturnLineFeed; + slice.NextChar(); // Skip \n + } + else + { + newline = Newline.CarriageReturn; + } + } + slice.NextChar(); // Skip \r or \n processor.Inline = new LineBreakInline { Span = { Start = processor.GetSourcePosition(startPosition, out int line, out int column) }, IsHard = EnableSoftAsHard || (slice.Start != 0 && hasDoubleSpacesBefore), Line = line, - Column = column + Column = column, + Newline = newline }; processor.Inline.Span.End = processor.Inline.Span.Start; return true; diff --git a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs index 7ec345eae..d826d2c70 100644 --- a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs @@ -77,7 +77,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var newSlice = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty; if (!newSlice.IsEmpty) { - processor.Inline = new LiteralInline() + processor.Inline = new LiteralInline { Content = length > 0 ? newSlice : StringSlice.Empty, Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)), diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index d7d0a2173..b7100479a 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -170,6 +170,7 @@ private BlockState TryContinueListItem(BlockProcessor state, ListItemBlock listI // Update list-item source end position listItem.UpdateSpanEnd(state.Line.End); + listItem.Newline = state.Line.Newline; return BlockState.Continue; } @@ -273,6 +274,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) BeforeWhitespace = whitespaceBefore, Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), + Newline = state.Line.Newline, }; state.NewBlocks.Push(newListItem); diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 500d57674..240a4c68c 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -120,7 +120,6 @@ private void ProcessBlocks() if (lastBlock != null) { lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); - lastBlock.LinesAfter.Add(lineText); } } break; diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 57a31d3a8..4fe317de5 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -28,7 +28,8 @@ public override BlockState TryOpen(BlockProcessor processor) { Column = processor.Column, Span = new SourceSpan(processor.Line.Start, processor.Line.End), - LinesBefore = processor.UseLinesBefore() + LinesBefore = processor.UseLinesBefore(), + Newline = processor.Line.Newline, }); return BlockState.Continue; } @@ -44,7 +45,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { return TryParseSetexHeading(processor, block); } - + block.Newline = processor.Line.Newline; block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; } @@ -79,8 +80,9 @@ public override bool Close(BlockProcessor processor, Block block) private BlockState TryParseSetexHeading(BlockProcessor state, Block block) { var line = state.Line; - - char headingChar = GetHeadingChar(ref line); + var sourcePosition = line.Start; + int count = 0; + char headingChar = GetHeadingChar(ref line, ref count); if (headingChar != 0) { @@ -93,6 +95,11 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) // We discard the paragraph that will be transformed to a heading state.Discard(paragraph); + while (state.CurrentChar == headingChar) + { + state.NextChar(); + } + int level = headingChar == '=' ? 1 : 2; var heading = new HeadingBlock(this) @@ -101,8 +108,13 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Span = new SourceSpan(paragraph.Span.Start, line.Start), Level = level, Lines = paragraph.Lines, - BeforeWhitespace = state.PopBeforeWhitespace(state.Column), + BeforeWhitespace = state.PopBeforeWhitespace(sourcePosition - 1), + AfterWhitespace = new StringSlice(state.Line.Text, state.Start, line.End), LinesBefore = state.UseLinesBefore(), + Newline = state.Line.Newline, + IsSetext = true, + HeaderCharCount = count, + SetextNewline = paragraph.Newline, }; //heading.Lines.Trim(); @@ -118,13 +130,13 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) return BlockState.Continue; } - private static char GetHeadingChar(ref StringSlice line) + private static char GetHeadingChar(ref StringSlice line, ref int count) { char headingChar = line.CurrentChar; if (headingChar == '=' || headingChar == '-') { - line.CountAndSkipChar(headingChar); + count = line.CountAndSkipChar(headingChar); if (line.IsEmpty) { diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 7c3d62e85..d484fbe53 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -54,7 +54,8 @@ public override BlockState TryOpen(BlockProcessor processor) quoteBlock.QuoteLines.Add(new QuoteBlock.QuoteLine { BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), - QuoteChar = true + QuoteChar = true, + Newline = processor.Line.Newline, }); processor.NewBlocks.Push(quoteBlock); processor.WhitespaceStart = sourcePosition + 1; @@ -85,6 +86,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { QuoteChar = false, BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + Newline = processor.Line.Newline, }); return BlockState.None; } @@ -93,6 +95,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { QuoteChar = true, BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + Newline = processor.Line.Newline, }); processor.NextChar(); // Skip quote marker char diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index e170b96c0..0fa5b1468 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -95,7 +95,8 @@ public override BlockState TryOpen(BlockProcessor processor) //BeforeWhitespace = beforeWhitespace, //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), LinesBefore = processor.UseLinesBefore(), - Content = new StringSlice(line.Text, processor.CurrentLineStartPosition, line.End) + Content = new StringSlice(line.Text, processor.CurrentLineStartPosition, line.End, line.Newline), + Newline = processor.Line.Newline, }); return BlockState.BreakDiscard; } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 8bc13cca6..0ca0abbe9 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -55,11 +55,12 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.Write(attributes); } */ - renderer.WriteLine(); + renderer.WriteLine(fencedCodeBlock.InfoNewline); renderer.WriteLeafRawLines(obj, true); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); + renderer.WriteLine(obj.Newline); renderer.Write(obj.AfterWhitespace); } else @@ -72,7 +73,8 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.PushIndent(indents); WriteLeafRawLines(renderer, obj); renderer.PopIndent(); - renderer.RenderLineAfterIfNeeded(obj); + + // ignore block newline, as last line references it } renderer.RenderLinesAfter(obj); @@ -86,11 +88,9 @@ public void WriteLeafRawLines(NormalizeRenderer renderer, LeafBlock leafBlock) var slices = lines.Lines; for (int i = 0; i < lines.Count; i++) { - if (i > 0) - { - renderer.WriteLine(); - } + var slice = slices[i].Slice; renderer.Write(ref slices[i].Slice); + renderer.WriteLine(slice.Newline); } } } diff --git a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs index 249f8aca6..70d8bee10 100644 --- a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs @@ -23,19 +23,36 @@ public class HeadingRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) { - renderer.RenderLinesBefore(obj); + if (obj.IsSetext) + { + renderer.RenderLinesBefore(obj); - var headingText = obj.Level > 0 && obj.Level <= 6 - ? HeadingTexts[obj.Level - 1] - : new string('#', obj.Level); + var headingChar = obj.Level == 1 ? '=' : '-'; + var line = new string(headingChar, obj.HeaderCharCount); - renderer.Write(obj.BeforeWhitespace); - renderer.Write(headingText).Write(' '); - renderer.WriteLeafInline(obj); + renderer.WriteLeafInline(obj); + renderer.WriteLine(obj.SetextNewline); + renderer.Write(obj.BeforeWhitespace); + renderer.Write(line); + renderer.WriteLine(obj.Newline); + renderer.Write(obj.AfterWhitespace); - renderer.FinishBlock(); + renderer.RenderLinesAfter(obj); + } + else + { + renderer.RenderLinesBefore(obj); - renderer.RenderLinesAfter(obj); + var headingText = obj.Level > 0 && obj.Level <= 6 + ? HeadingTexts[obj.Level - 1] + : new string('#', obj.Level); + + renderer.Write(obj.BeforeWhitespace); + renderer.Write(headingText).Write(' '); + renderer.WriteLeafInline(obj); + + renderer.RenderLinesAfter(obj); + } } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index a34d72449..9c27f0323 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -12,7 +12,7 @@ protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { renderer.RenderLinesBefore(obj); renderer.WriteLeafRawLines(obj, false, false); - renderer.RenderLineAfterIfNeeded(obj); + renderer.WriteLine(obj.Newline); renderer.RenderLinesAfter(obj); } } diff --git a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs index 59302546a..34893e6e1 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs @@ -24,7 +24,7 @@ protected override void Write(NormalizeRenderer renderer, LineBreakInline obj) renderer.Write("\\"); //renderer.Write(obj.IsBackslash ? "\\" : " "); } - renderer.WriteLine(); + renderer.WriteLine(obj.Newline); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 18ac53d7d..286704588 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -65,13 +65,8 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } else { - var writeLine = false; for (var i = 0; i < listBlock.Count; i++) { - if (writeLine) - { - renderer.WriteLine(); - } var item = listBlock[i]; var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); @@ -85,16 +80,10 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) renderer.PopIndent(); renderer.RenderLinesAfter(listItem); - writeLine = true; } } renderer.CompactParagraph = compact; - // TODO: make reusable method? - if (listBlock.Parent is MarkdownDocument md && md.LastChild != listBlock) - { - renderer.WriteLine(); - } renderer.RenderLinesAfter(listBlock); } diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index c80e62391..d58acff1f 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -134,22 +134,15 @@ public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfL var slices = lines.Lines; for (int i = 0; i < lines.Count; i++) { - if (!writeEndOfLines && i > 0) - { - WriteLine(); - } if (indent) { Write(" "); } - Write(ref slices[i].Slice); - - if (writeEndOfLines) - { - WriteLine(); - } + var slice = slices[i].Slice; + Write(ref slice); + WriteLine(slice.Newline); } } return this; @@ -161,7 +154,7 @@ public void RenderLinesBefore(Block block) { foreach (var line in block.LinesBefore) { - WriteLine(line.ToString()); + WriteLine(line.Newline); } } } @@ -172,19 +165,9 @@ public void RenderLinesAfter(Block block) { foreach (var line in block.LinesAfter) { - WriteLine(line.ToString()); + WriteLine(line.Newline); } } } - - public void RenderLineAfterIfNeeded(Block block) - { - bool isLast = Equals(block.Parent.LastChild, block); - bool hasLinesAfter = block.LinesAfter != null && block.LinesAfter.Count > 0; - if (!isLast && !hasLinesAfter) - { - WriteLine(); - } - } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 5d02108f5..d7f28778a 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -19,7 +19,6 @@ protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragra renderer.RenderLinesBefore(paragraph); renderer.Write(paragraph.BeforeWhitespace); renderer.WriteLeafInline(paragraph); - renderer.RenderLineAfterIfNeeded(paragraph); renderer.RenderLinesAfter(paragraph); } } diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 79ed21b03..02612a2d0 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -25,10 +25,9 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) } renderer.PushIndent(indents); - renderer.WriteChildren(quoteBlock); + renderer.WriteChildren(quoteBlock); renderer.PopIndent(); - renderer.FinishBlock(); renderer.RenderLinesAfter(quoteBlock); } } diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 6f5e8001b..563b387d7 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -22,7 +22,7 @@ protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj //renderer.Write(obj.BeforeWhitespace); renderer.Write(obj.Content); //renderer.Write(obj.AfterWhitespace); - renderer.RenderLineAfterIfNeeded(obj); + renderer.WriteLine(obj.Newline); renderer.RenderLinesAfter(obj); } } diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 7bac70999..a1f6ac2c4 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -286,6 +286,20 @@ public T WriteLine() return (T) this; } + /// + /// Writes a newline. + /// + /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T WriteLine(Newline newline) + { + WriteIndent(); + Writer.NewLine = newline; + Writer.WriteLine(); + previousWasLine = true; + return (T)this; + } + /// /// Writes a content followed by a newline. /// diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index 282c7d022..d9e4f96b6 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -45,6 +45,11 @@ protected Block(BlockParser parser) /// public bool IsBreakable { get; set; } + /// + /// The last newline of this block + /// + public Newline Newline { get; set; } + /// /// Gets or sets a value indicating whether this block must be removed from its container after inlines have been processed. /// diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index ad6680e0a..66014bf86 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; namespace Markdig.Syntax @@ -59,6 +60,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// public string WhitespaceAfterFencedChar { get; set; } + public Newline InfoNewline { get; set; } /// /// Gets or sets the indent count when the fenced code block was indented diff --git a/src/Markdig/Syntax/HeadingBlock.cs b/src/Markdig/Syntax/HeadingBlock.cs index 7b1b453b8..ccd593159 100644 --- a/src/Markdig/Syntax/HeadingBlock.cs +++ b/src/Markdig/Syntax/HeadingBlock.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using System.Diagnostics; +using Markdig.Helpers; using Markdig.Parsers; namespace Markdig.Syntax @@ -31,5 +32,11 @@ public HeadingBlock(BlockParser parser) : base(parser) /// Gets or sets the level of heading (starting at 1 for the lowest level). /// public int Level { get; set; } + + public bool IsSetext { get; set; } + + public int HeaderCharCount { get; set; } + + public Newline SetextNewline { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 729fd1c72..0dd65a8a7 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -2,6 +2,8 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; + namespace Markdig.Syntax { /// @@ -53,5 +55,8 @@ public interface IFencedBlock : IBlock /// Gets or sets the fenced character used to open and close this fenced code block. /// char FencedChar { get; set; } + + Newline Newline { get; set; } + Newline InfoNewline { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LineBreakInline.cs b/src/Markdig/Syntax/Inlines/LineBreakInline.cs index 3135a2d6f..8093f950c 100644 --- a/src/Markdig/Syntax/Inlines/LineBreakInline.cs +++ b/src/Markdig/Syntax/Inlines/LineBreakInline.cs @@ -2,6 +2,8 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; + namespace Markdig.Syntax.Inlines { /// @@ -13,5 +15,7 @@ public class LineBreakInline : LeafInline public bool IsHard { get; set; } public bool IsBackslash { get; set; } + + public Newline Newline { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index a02f466dd..7133c80e3 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -60,7 +60,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi // if not first line, preserve whitespace c = 0; } - var stringLine = new StringLine(ref slice, line, c, sourceLinePosition); + var stringLine = new StringLine(ref slice, line, c, sourceLinePosition, slice.Newline); // Regular case, we are not in the middle of a tab if (slice.CurrentChar != '\t' || !CharHelper.IsAcrossTab(column)) { @@ -76,6 +76,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi stringLine.Slice = new StringSlice(builder.GetStringAndReset()); Lines.Add(ref stringLine); } + Newline = slice.Newline; // update newline, as it should be the last newline of the block } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index 730835491..9c32eccc7 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -21,6 +21,8 @@ public class QuoteLine public StringSlice AfterWhitespace { get; set; } + public Newline Newline { get; set; } + // support lazy lines public bool QuoteChar { get; set; } } From c9fc6085982d085613924d64face069b81672aa1 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 11 Oct 2020 14:07:32 +0200 Subject: [PATCH 038/120] update todo --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 1c250e9d9..28559a7bb 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -14,22 +14,25 @@ Quoteblocks may have different syntactical characters applied per line. That is, # TODO In order: - ~~`p\n p`: affects many tests~~ -- `\r\n` and `\r` support -- support SetextHeading -- support link parsing +- ~~`\r\n` and `\r` support~~ +- ~~support SetextHeading~~ - support LinkReferenceDefinition -- fix broken pre-existing tests -- fix `TODO: RTP: ` +- support link parsing +- `\0` - generate spec examples as tests for roundtrip -- run pull request feedback +- fix `TODO: RTP: ` - introduce feature flag - extract MarkdownRenderer +- cleanup NormalizeRenderer (MarkdownRenderer) - deduplicate MarkdownRenderer and NormalizeRenderer code -- `\0` +- do pull request feedback +- split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? +- fix broken pre-existing tests - support extensions - review complete PR and follow conventions - run perf test -- document how trivia are handled generically and specifically - create todo list with perf optimization focus points +- optimize perf +- document how trivia are handled generically and specifically - write tree comparison tests? - write tree visualization tool? From 5e15575929cc72a6a965ad1610e715a5d06d61ed Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 17 Oct 2020 15:48:36 +0200 Subject: [PATCH 039/120] implement LinkReferenceDefinition --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 3 + .../TestLinkReferenceDefinition.cs | 188 +++++++++++- src/Markdig/Helpers/LinkHelper.cs | 274 +++++++++++++++++- src/Markdig/Helpers/StringLineGroup.cs | 18 +- src/Markdig/Helpers/StringSlice.cs | 1 + src/Markdig/Parsers/BlockProcessor.cs | 7 +- src/Markdig/Parsers/ParagraphBlockParser.cs | 53 +++- .../LinkReferenceDefinitionRenderer.cs | 16 +- src/Markdig/Syntax/Block.cs | 1 + src/Markdig/Syntax/CharIteratorHelper.cs | 3 +- src/Markdig/Syntax/LinkReferenceDefinition.cs | 59 +++- 11 files changed, 590 insertions(+), 33 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 28559a7bb..44340ba72 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -16,6 +16,8 @@ In order: - ~~`p\n p`: affects many tests~~ - ~~`\r\n` and `\r` support~~ - ~~support SetextHeading~~ +- check char.IsWhitespace() calls +- check char.IsNewline() calls - support LinkReferenceDefinition - support link parsing - `\0` @@ -33,6 +35,7 @@ In order: - run perf test - create todo list with perf optimization focus points - optimize perf +- merge from main - document how trivia are handled generically and specifically - write tree comparison tests? - write tree visualization tool? diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index 963a5831f..7901ecb33 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -1,10 +1,190 @@ -using System; -using System.Collections.Generic; -using System.Text; +using NUnit.Framework; +using static Markdig.Tests.RoundtripSpecs.TestHelper; namespace Markdig.Tests.RoundtripSpecs { - class TestLinkReferenceDefinition + // TODO: RTP: ".[a] /r" dont seem to be parsed into LRD! + [TestFixture] + public class TestLinkReferenceDefinition { + [TestCase(@"[a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + + [TestCase(@"[a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + [TestCase(@" [a]: /r")] + + [TestCase(@"[a]: /r ")] + [TestCase(@" [a]: /r ")] + [TestCase(@" [a]: /r ")] + [TestCase(@" [a]: /r ")] + + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l"" ")] + [TestCase(@"[a]: /r ""l""")] + [TestCase(@"[a]: /r ""l"" ")] + + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + [TestCase(@" [a]: /r ""l""")] + [TestCase(@" [a]: /r ""l"" ")] + + [TestCase("[a]:\t/r")] + [TestCase("[a]:\t/r\t")] + [TestCase("[a]:\t/r\t\"l\"")] + [TestCase("[a]:\t/r\t\"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r\t")] + [TestCase("[a]: \t/r\t\"l\"")] + [TestCase("[a]: \t/r\t\"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r\t")] + [TestCase("[a]:\t /r\t\"l\"")] + [TestCase("[a]:\t /r\t\"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r\t")] + [TestCase("[a]: \t /r\t\"l\"")] + [TestCase("[a]: \t /r\t\"l\"\t")] + + [TestCase("[a]:\t/r \t")] + [TestCase("[a]:\t/r \t\"l\"")] + [TestCase("[a]:\t/r \t\"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r \t")] + [TestCase("[a]: \t/r \t\"l\"")] + [TestCase("[a]: \t/r \t\"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r \t")] + [TestCase("[a]:\t /r \t\"l\"")] + [TestCase("[a]:\t /r \t\"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r \t")] + [TestCase("[a]: \t /r \t\"l\"")] + [TestCase("[a]: \t /r \t\"l\"\t")] + + [TestCase("[a]:\t/r\t ")] + [TestCase("[a]:\t/r\t \"l\"")] + [TestCase("[a]:\t/r\t \"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r\t ")] + [TestCase("[a]: \t/r\t \"l\"")] + [TestCase("[a]: \t/r\t \"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r\t ")] + [TestCase("[a]:\t /r\t \"l\"")] + [TestCase("[a]:\t /r\t \"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r\t ")] + [TestCase("[a]: \t /r\t \"l\"")] + [TestCase("[a]: \t /r\t \"l\"\t")] + + [TestCase("[a]:\t/r \t ")] + [TestCase("[a]:\t/r \t \"l\"")] + [TestCase("[a]:\t/r \t \"l\"\t")] + + [TestCase("[a]: \t/r")] + [TestCase("[a]: \t/r \t ")] + [TestCase("[a]: \t/r \t \"l\"")] + [TestCase("[a]: \t/r \t \"l\"\t")] + + [TestCase("[a]:\t /r")] + [TestCase("[a]:\t /r \t ")] + [TestCase("[a]:\t /r \t \"l\"")] + [TestCase("[a]:\t /r \t \"l\"\t")] + + [TestCase("[a]: \t /r")] + [TestCase("[a]: \t /r \t ")] + [TestCase("[a]: \t /r \t \"l\"")] + [TestCase("[a]: \t /r \t \"l\"\t")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("[a]:\f/r\f\"l\"")] + [TestCase("[a]:\v/r\v\"l\"")] + public void TestUncommonWhitespace(string value) + { + RoundTrip(value); + } + + [TestCase("[a]:\n/r\n\"t\"")] + [TestCase("[a]:\n/r\r\"t\"")] + [TestCase("[a]:\n/r\r\n\"t\"")] + + [TestCase("[a]:\r/r\n\"t\"")] + [TestCase("[a]:\r/r\r\"t\"")] + [TestCase("[a]:\r/r\r\n\"t\"")] + + [TestCase("[a]:\r\n/r\n\"t\"")] + [TestCase("[a]:\r\n/r\r\"t\"")] + [TestCase("[a]:\r\n/r\r\n\"t\"")] + + [TestCase("[a]:\n/r\n\"t\nt\"")] + [TestCase("[a]:\n/r\n\"t\rt\"")] + [TestCase("[a]:\n/r\n\"t\r\nt\"")] + + [TestCase("[a]:\r\n /r\t \n \t \"t\r\nt\" ")] + public void TestNewlines(string value) + { + RoundTrip(value); + } + + [TestCase("[ a]: /r")] + [TestCase("[a ]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[ a]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[a ]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[ a ]: /r")] + [TestCase("[a a]: /r")] + [TestCase("[a\va]: /r")] + [TestCase("[a\fa]: /r")] + [TestCase("[a\ta]: /r")] + [TestCase("[\va]: /r")] + [TestCase("[\fa]: /r")] + [TestCase("[\ta]: /r")] + [TestCase(@"[\]]: /r")] + public void TestLabel(string value) + { + RoundTrip(value); + } + + [TestCase("[a]: /r\n===\n[a]")] + public void TestSetextHeader(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 02aec0a9e..5dfd20a0c 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; +using System.Text; using Markdig.Syntax; namespace Markdig.Helpers @@ -439,7 +440,7 @@ public static bool TryParseTitle(ref T text, out string title) where T : ICha { c = text.NextChar(); - if (c == '\n') + if (c == '\r' || c == '\n') { if (hasOnlyWhiteSpacesSinceLastLine >= 0) { @@ -449,6 +450,12 @@ public static bool TryParseTitle(ref T text, out string title) where T : ICha } hasOnlyWhiteSpacesSinceLastLine = -1; } + buffer.Append(c); + if (c == '\r' && text.PeekChar() == '\n') + { + buffer.Append('\n'); + } + continue; } if (c == '\0') @@ -491,7 +498,7 @@ public static bool TryParseTitle(ref T text, out string title) where T : ICha hasOnlyWhiteSpacesSinceLastLine = 1; } } - else if (c != '\n') + else if (c != '\n' && c != '\r' && (c != '\r' && text.PeekChar() != '\n')) { hasOnlyWhiteSpacesSinceLastLine = 0; } @@ -712,18 +719,19 @@ public static bool IsValidDomain(string link, int prefixLength) segmentCount - lastUnderscoreSegment >= 2; // No underscores are present in the last two segments of the domain } - public static bool TryParseLinkReferenceDefinition(T text, out string label, out string url, - out string title) where T : ICharIterator - { - return TryParseLinkReferenceDefinition(ref text, out label, out url, out title); - } + // apparently, below code is dead code (except for public api?) + //public static bool TryParseLinkReferenceDefinition(T text, out string label, out string url, + // out string title) where T : ICharIterator + //{ + // return TryParseLinkReferenceDefinition(ref text, out label, out url, out title); + //} - public static bool TryParseLinkReferenceDefinition(ref T text, out string label, out string url, out string title) - where T : ICharIterator - { - return TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out SourceSpan labelSpan, out SourceSpan urlSpan, - out SourceSpan titleSpan); - } + //public static bool TryParseLinkReferenceDefinition(ref T text, out string label, out string url, out string title) + // where T : ICharIterator + //{ + // return TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out SourceSpan labelSpan, out SourceSpan urlSpan, + // out SourceSpan titleSpan); + //} public static bool TryParseLinkReferenceDefinition(ref T text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan) where T : ICharIterator { @@ -811,6 +819,116 @@ public static bool TryParseLinkReferenceDefinition(ref T text, out string lab return true; } + public static bool TryParseLinkReferenceDefinitionWhitespace( + ref T text, + out SourceSpan whitespaceBeforeLabel, + out string label, + out string labelWithWhitespace, + out SourceSpan whitespaceBeforeUrl, // can contain newline + out string url, + out SourceSpan whitespaceBeforeTitle, // can contain newline + out string title, // can contain non-consecutive newlines + out SourceSpan whitespaceAfterTitle, + out SourceSpan labelSpan, + out SourceSpan urlSpan, + out SourceSpan titleSpan) where T : ICharIterator + { + whitespaceBeforeUrl = SourceSpan.Empty; + url = null; + whitespaceBeforeTitle = SourceSpan.Empty; + title = null; + + urlSpan = SourceSpan.Empty; + titleSpan = SourceSpan.Empty; + + text.TrimStart(); + whitespaceBeforeLabel = new SourceSpan(0, text.Start - 1); + whitespaceAfterTitle = SourceSpan.Empty; + + if (!TryParseLabelWhitespace(ref text, out label, out labelWithWhitespace, out labelSpan)) + { + return false; + } + + if (text.CurrentChar != ':') + { + label = null; + return false; + } + text.NextChar(); // Skip ':' + var beforeWhitespaceUrlStart = text.Start; + + // Skip any whitespace before the url + text.TrimStart(); + whitespaceBeforeUrl = new SourceSpan(beforeWhitespaceUrlStart, text.Start - 1); + + urlSpan.Start = text.Start; + bool isAngleBracketsUrl = text.CurrentChar == '<'; + if (!TryParseUrl(ref text, out url) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + { + return false; + } + urlSpan.End = text.Start - 1; + int whitespaceBeforeTitleStart = text.Start; + + var saved = text; + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount); + + whitespaceBeforeTitle = new SourceSpan(whitespaceBeforeTitleStart, text.Start - 1); + var c = text.CurrentChar; + if (c == '\'' || c == '"' || c == '(') + { + titleSpan.Start = text.Start; + if (TryParseTitle(ref text, out title)) + { + titleSpan.End = text.Start - 1; + // If we have a title, it requires a whitespace after the url + if (!hasWhiteSpaces) + { + return false; + } + whitespaceAfterTitle = new SourceSpan(text.Start, text.End); + } + else + { + return false; + } + } + else + { + if (text.CurrentChar == '\0' || newLineCount > 0) + { + return true; + } + } + + // Check that the current line has only trailing spaces + c = text.CurrentChar; + while (c.IsSpaceOrTab()) + { + c = text.NextChar(); + } + + if (c != '\0' && c != '\n' && c != '\r' && (c != '\r' && text.PeekChar() != '\n')) + { + // If we were able to parse the url but the title doesn't end with space, + // we are still returning a valid definition + if (newLineCount > 0 && title != null) + { + text = saved; + title = null; + return true; + } + + label = null; + url = null; + title = null; + return false; + } + + return true; + } + public static bool TryParseLabel(T lines, out string label) where T : ICharIterator { return TryParseLabel(ref lines, false, out label, out SourceSpan labelSpan); @@ -831,6 +949,11 @@ public static bool TryParseLabel(ref T lines, out string label, out SourceSpa return TryParseLabel(ref lines, false, out label, out labelSpan); } + public static bool TryParseLabelWhitespace(ref T lines, out string label, out string labelWithWhitespace, out SourceSpan labelSpan) where T : ICharIterator + { + return TryParseLabelWhitespace(ref lines, false, out label, out labelWithWhitespace, out labelSpan); + } + public static bool TryParseLabel(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator { label = null; @@ -945,5 +1068,130 @@ public static bool TryParseLabel(ref T lines, bool allowEmpty, out string lab return isValid; } + + public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out string label, out string labelWithWhitespace, out SourceSpan labelSpan) where T : ICharIterator + { + label = null; + labelWithWhitespace = null; + char c = lines.CurrentChar; + labelSpan = SourceSpan.Empty; + if (c != '[') + { + return false; + } + var buffer = StringBuilderCache.Local(); + var bufferWhitespace = new StringBuilder(); + + var startLabel = -1; + var endLabel = -1; + + bool hasEscape = false; + bool previousWhitespace = true; + bool hasNonWhiteSpace = false; + bool isValid = false; + while (true) + { + c = lines.NextChar(); + if (c == '\0') + { + break; + } + + if (hasEscape) + { + if (c != '[' && c != ']' && c != '\\') + { + break; + } + } + else + { + if (c == '[') + { + break; + } + + if (c == ']') + { + lines.NextChar(); // Skip ] + if (allowEmpty || hasNonWhiteSpace) + { + // Remove trailing spaces + for (int i = buffer.Length - 1; i >= 0; i--) + { + if (!buffer[i].IsWhitespace()) + { + break; + } + buffer.Length = i; + endLabel--; + } + + // Only valid if buffer is less than 1000 characters + if (buffer.Length <= 999) + { + labelSpan.Start = startLabel; + labelSpan.End = endLabel; + if (labelSpan.Start > labelSpan.End) + { + labelSpan = SourceSpan.Empty; + } + + label = buffer.ToString(); + labelWithWhitespace = bufferWhitespace.ToString(); + isValid = true; + } + } + break; + } + } + + var isWhitespace = c.IsWhitespace(); + + + if (!hasEscape && c == '\\') + { + if (startLabel < 0) + { + startLabel = lines.Start; + } + hasEscape = true; + bufferWhitespace.Append(c); + } + else + { + hasEscape = false; + + if (!previousWhitespace || !isWhitespace) + { + if (startLabel < 0) + { + startLabel = lines.Start; + } + endLabel = lines.Start; + if (isWhitespace) + { + // Replace any whitespace by a single ' ' + buffer.Append(' '); + } + else + { + buffer.Append(c); + } + if (!isWhitespace) + { + hasNonWhiteSpace = true; + } + } + bufferWhitespace.Append(c); + } + previousWhitespace = isWhitespace; + } + + buffer.Length = 0; + + return isValid; + } + } } \ No newline at end of file diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index 8afab5e1f..03011b7ee 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -221,10 +221,11 @@ public Iterator(StringLineGroup lines) _offset = -1; SliceIndex = 0; CurrentChar = '\0'; - End = -2; + End = -1; for (int i = 0; i < lines.Count; i++) { - End += lines.Lines[i].Slice.Length + 1; // Add chars + var line = lines.Lines[i]; + End += line.Slice.Length + line.Newline.Length; // Add chars } NextChar(); } @@ -276,9 +277,16 @@ public char NextChar() } else { - CurrentChar = '\n'; - SliceIndex++; - _offset = -1; + CurrentChar = slice[slice.Start + _offset]; + if (CurrentChar == '\r' && slice[slice.Start + _offset + 1] == '\n') + { + + } + else + { + SliceIndex++; + _offset = -1; + } } } else diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 49c15d7d7..bc941bc60 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -42,6 +42,7 @@ public static implicit operator string (Newline newline) } return string.Empty; } + public int Length => (carriageReturn ? 1 : 0) + (lineFeed ? 1 : 0); } /// diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 35dbd5623..f7222efe9 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -196,6 +196,7 @@ public StringSlice PopBeforeWhitespace(int start, int end) } public List BeforeLines { get; set; } + public bool TrackTrivia { get => trackTrivia; set => trackTrivia = value; } /// /// Get the current Container that is currently opened @@ -670,7 +671,7 @@ private void TryContinueBlocks() ContinueProcessingLine = false; if (!result.IsDiscard()) { - if (trackTrivia) + if (TrackTrivia) { //UnwindAllIndents(); } @@ -813,7 +814,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (!result.IsDiscard()) { // TODO: RTP: pass line with whitespace - if (trackTrivia) + if (TrackTrivia) { UnwindAllIndents(); } @@ -877,7 +878,7 @@ private void ProcessNewBlocks(BlockState result, bool allowClosing) if (!result.IsDiscard()) { bool isParagraph = block is ParagraphBlock; - if (trackTrivia && isParagraph) + if (TrackTrivia && isParagraph) { UnwindAllIndents(); } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 4fe317de5..c042146e0 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -56,7 +56,14 @@ public override bool Close(BlockProcessor processor, Block block) { ref var lines = ref paragraph.Lines; - TryMatchLinkReferenceDefinition(ref lines, processor); + if (processor.TrackTrivia) + { + TryMatchLinkReferenceDefinitionWhitespace(ref lines, processor); + } + else + { + TryMatchLinkReferenceDefinition(ref lines, processor); + } int lineCount = lines.Count; @@ -188,5 +195,49 @@ private static bool TryMatchLinkReferenceDefinition(ref StringLineGroup lines, B return atLeastOneFound; } + + private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGroup lines, BlockProcessor state) + { + bool atLeastOneFound = false; + + while (true) + { + // If we have found a LinkReferenceDefinition, we can discard the previous paragraph + var iterator = lines.ToCharIterator(); + if (LinkReferenceDefinition.TryParseWhitespace( + ref iterator, + out LinkReferenceDefinition lrd, + out SourceSpan whitespaceBeforeLabel, + out SourceSpan whitespaceBeforeUrl, + out SourceSpan whitespaceBeforeTitle, + out SourceSpan whitespaceAfterTitle)) + { + state.Document.SetLinkReferenceDefinition(lrd.Label, lrd); + atLeastOneFound = true; + + // Correct the locations of each field + lrd.Line = lines.Lines[0].Line; + var text = lines.ToString(); + int startPosition = lines.Lines[0].Slice.Start; + + lrd.Span = lrd.Span.MoveForward(startPosition); + lrd.BeforeWhitespace = new StringSlice(text, whitespaceBeforeLabel.Start, whitespaceBeforeLabel.End); + lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); + lrd.WhitespaceBeforeUrl = new StringSlice(text, whitespaceBeforeUrl.Start, whitespaceBeforeUrl.End); + lrd.UrlSpan = lrd.UrlSpan.MoveForward(startPosition); + lrd.WhitespaceBeforeTitle = new StringSlice(text, whitespaceBeforeTitle.Start, whitespaceBeforeTitle.End); + lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); + lrd.AfterWhitespace = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + + lines = iterator.Remaining(); + } + else + { + break; + } + } + + return atLeastOneFound; + } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs index c09a9bbde..f6fa5da22 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs @@ -10,20 +10,26 @@ public class LinkReferenceDefinitionRenderer : NormalizeObjectRenderer public bool RemoveAfterProcessInlines { get; set; } + // TODO: RTP: rename to WhitespaceBefore public StringSlice BeforeWhitespace { get; set; } public StringSlice AfterWhitespace { get; set; } diff --git a/src/Markdig/Syntax/CharIteratorHelper.cs b/src/Markdig/Syntax/CharIteratorHelper.cs index 74b215d91..4e8939c7a 100644 --- a/src/Markdig/Syntax/CharIteratorHelper.cs +++ b/src/Markdig/Syntax/CharIteratorHelper.cs @@ -18,7 +18,8 @@ public static bool TrimStartAndCountNewLines(ref T iterator, out int countNew bool hasWhitespaces = false; while (c.IsWhitespace()) { - if (c == '\n') + // TODO: RTP: fix newline check here for \r\n + if (c == '\n' || c == '\r') { countNewLines++; } diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 7c054f700..b0ae6ba09 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -45,15 +45,25 @@ public LinkReferenceDefinition(string label, string url, string title) : this() } /// - /// Gets or sets the label. + /// Gets or sets the label. Text is normalized according to spec. /// + /// public string Label { get; set; } + /// + /// Non-normalized Label (includes whitespace) + /// + public string LabelWithWhitespace { get; set; } + + public StringSlice WhitespaceBeforeUrl { get; set; } + /// /// Gets or sets the URL. /// public string Url { get; set; } + public StringSlice WhitespaceBeforeTitle { get; set; } + /// /// Gets or sets the title. /// @@ -110,5 +120,52 @@ public static bool TryParse(ref T text, out LinkReferenceDefinition block) wh }; return true; } + + /// + /// Tries to the parse the specified text into a definition. + /// + /// Type of the text + /// The text. + /// The block. + /// true if parsing is successful; false otherwise + public static bool TryParseWhitespace( + ref T text, + out LinkReferenceDefinition block, + out SourceSpan whitespaceBeforeLabel, + out SourceSpan whitespaceBeforeUrl, + out SourceSpan whitespaceBeforeTitle, + out SourceSpan whitespaceAfterTitle) where T : ICharIterator + { + block = null; + + var startSpan = text.Start; + + if (!LinkHelper.TryParseLinkReferenceDefinitionWhitespace( + ref text, + out whitespaceBeforeLabel, + out string label, + out string labelWithWhitespace, + out whitespaceBeforeUrl, + out string url, + out whitespaceBeforeTitle, + out string title, + out whitespaceAfterTitle, + out SourceSpan labelSpan, + out SourceSpan urlSpan, + out SourceSpan titleSpan)) + { + return false; + } + + block = new LinkReferenceDefinition(label, url, title) + { + LabelWithWhitespace = labelWithWhitespace, + LabelSpan = labelSpan, + UrlSpan = urlSpan, + TitleSpan = titleSpan, + Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End) + }; + return true; + } } } \ No newline at end of file From 49aa856f525cbd9602c95199150ec3c83a73c8ed Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 17 Oct 2020 23:09:53 +0200 Subject: [PATCH 040/120] implement LinkInline --- .../RoundtripSpecs/Inlines/TestLinkInline.cs | 247 ++++++++++++------ src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 20 +- .../TestLinkReferenceDefinition.cs | 16 ++ src/Markdig.Tests/TestLinkHelper.cs | 16 +- .../Extensions/AutoLinks/AutoLinkParser.cs | 2 +- src/Markdig/Helpers/LinkHelper.cs | 126 ++++++++- src/Markdig/Parsers/InlineProcessor.cs | 2 + .../Parsers/Inlines/LinkInlineParser.cs | 137 +++++++--- .../Normalize/Inlines/LinkInlineRenderer.cs | 32 ++- .../LinkReferenceDefinitionRenderer.cs | 12 +- .../Syntax/Inlines/LinkDelimiterInline.cs | 2 + src/Markdig/Syntax/Inlines/LinkInline.cs | 13 + src/Markdig/Syntax/LinkReferenceDefinition.cs | 8 + 13 files changed, 496 insertions(+), 137 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs index 89e8350f0..8c3a29d29 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -35,81 +35,178 @@ public class TestLinkInline [TestCase(" [ a ](b) ")] // below cases are required for a full CST but not have low prio for impl - //[TestCase("[]( b)")] - //[TestCase(" []( b)")] - //[TestCase("[]( b) ")] - //[TestCase(" []( b) ")] - - //[TestCase("[a]( b)")] - //[TestCase(" [a]( b)")] - //[TestCase("[a]( b) ")] - //[TestCase(" [a]( b) ")] - - //[TestCase("[ a]( b)")] - //[TestCase(" [ a]( b)")] - //[TestCase("[ a]( b) ")] - //[TestCase(" [ a]( b) ")] - - //[TestCase("[a ]( b)")] - //[TestCase(" [a ]( b)")] - //[TestCase("[a ]( b) ")] - //[TestCase(" [a ]( b) ")] - - //[TestCase("[ a ]( b)")] - //[TestCase(" [ a ]( b)")] - //[TestCase("[ a ]( b) ")] - //[TestCase(" [ a ]( b) ")] - - //[TestCase("[](b )")] - //[TestCase(" [](b )")] - //[TestCase("[](b ) ")] - //[TestCase(" [](b ) ")] - - //[TestCase("[a](b )")] - //[TestCase(" [a](b )")] - //[TestCase("[a](b ) ")] - //[TestCase(" [a](b ) ")] - - //[TestCase("[ a](b )")] - //[TestCase(" [ a](b )")] - //[TestCase("[ a](b ) ")] - //[TestCase(" [ a](b ) ")] - - //[TestCase("[a ](b )")] - //[TestCase(" [a ](b )")] - //[TestCase("[a ](b ) ")] - //[TestCase(" [a ](b ) ")] - - //[TestCase("[ a ](b )")] - //[TestCase(" [ a ](b )")] - //[TestCase("[ a ](b ) ")] - //[TestCase(" [ a ](b ) ")] - - //[TestCase("[]( b )")] - //[TestCase(" []( b )")] - //[TestCase("[]( b ) ")] - //[TestCase(" []( b ) ")] - - //[TestCase("[a]( b )")] - //[TestCase(" [a]( b )")] - //[TestCase("[a]( b ) ")] - //[TestCase(" [a]( b ) ")] - - //[TestCase("[ a]( b )")] - //[TestCase(" [ a]( b )")] - //[TestCase("[ a]( b ) ")] - //[TestCase(" [ a]( b ) ")] - - //[TestCase("[a ]( b )")] - //[TestCase(" [a ]( b )")] - //[TestCase("[a ]( b ) ")] - //[TestCase(" [a ]( b ) ")] - - //[TestCase("[ a ]( b )")] - //[TestCase(" [ a ]( b )")] - //[TestCase("[ a ]( b ) ")] - //[TestCase(" [ a ]( b ) ")] - public void Test_InlineLink(string value) + [TestCase("[]( b)")] + [TestCase(" []( b)")] + [TestCase("[]( b) ")] + [TestCase(" []( b) ")] + + [TestCase("[a]( b)")] + [TestCase(" [a]( b)")] + [TestCase("[a]( b) ")] + [TestCase(" [a]( b) ")] + + [TestCase("[ a]( b)")] + [TestCase(" [ a]( b)")] + [TestCase("[ a]( b) ")] + [TestCase(" [ a]( b) ")] + + [TestCase("[a ]( b)")] + [TestCase(" [a ]( b)")] + [TestCase("[a ]( b) ")] + [TestCase(" [a ]( b) ")] + + [TestCase("[ a ]( b)")] + [TestCase(" [ a ]( b)")] + [TestCase("[ a ]( b) ")] + [TestCase(" [ a ]( b) ")] + + [TestCase("[](b )")] + [TestCase(" [](b )")] + [TestCase("[](b ) ")] + [TestCase(" [](b ) ")] + + [TestCase("[a](b )")] + [TestCase(" [a](b )")] + [TestCase("[a](b ) ")] + [TestCase(" [a](b ) ")] + + [TestCase("[ a](b )")] + [TestCase(" [ a](b )")] + [TestCase("[ a](b ) ")] + [TestCase(" [ a](b ) ")] + + [TestCase("[a ](b )")] + [TestCase(" [a ](b )")] + [TestCase("[a ](b ) ")] + [TestCase(" [a ](b ) ")] + + [TestCase("[ a ](b )")] + [TestCase(" [ a ](b )")] + [TestCase("[ a ](b ) ")] + [TestCase(" [ a ](b ) ")] + + [TestCase("[]( b )")] + [TestCase(" []( b )")] + [TestCase("[]( b ) ")] + [TestCase(" []( b ) ")] + + [TestCase("[a]( b )")] + [TestCase(" [a]( b )")] + [TestCase("[a]( b ) ")] + [TestCase(" [a]( b ) ")] + + [TestCase("[ a]( b )")] + [TestCase(" [ a]( b )")] + [TestCase("[ a]( b ) ")] + [TestCase(" [ a]( b ) ")] + + [TestCase("[a ]( b )")] + [TestCase(" [a ]( b )")] + [TestCase("[a ]( b ) ")] + [TestCase(" [a ]( b ) ")] + + [TestCase("[ a ]( b )")] + [TestCase(" [ a ]( b )")] + [TestCase("[ a ]( b ) ")] + [TestCase(" [ a ]( b ) ")] + public void Test(string value) + { + RoundTrip(value); + } + + [TestCase("[a](b \"t\") ")] + [TestCase("[a](b \" t\") ")] + [TestCase("[a](b \"t \") ")] + [TestCase("[a](b \" t \") ")] + + [TestCase("[a](b \"t\") ")] + [TestCase("[a](b \" t\") ")] + [TestCase("[a](b \"t \") ")] + [TestCase("[a](b \" t \") ")] + + [TestCase("[a](b \"t\" ) ")] + [TestCase("[a](b \" t\" ) ")] + [TestCase("[a](b \"t \" ) ")] + [TestCase("[a](b \" t \" ) ")] + + [TestCase("[a](b \"t\" ) ")] + [TestCase("[a](b \" t\" ) ")] + [TestCase("[a](b \"t \" ) ")] + [TestCase("[a](b \" t \" ) ")] + + [TestCase("[a](b 't') ")] + [TestCase("[a](b ' t') ")] + [TestCase("[a](b 't ') ")] + [TestCase("[a](b ' t ') ")] + + [TestCase("[a](b 't') ")] + [TestCase("[a](b ' t') ")] + [TestCase("[a](b 't ') ")] + [TestCase("[a](b ' t ') ")] + + [TestCase("[a](b 't' ) ")] + [TestCase("[a](b ' t' ) ")] + [TestCase("[a](b 't ' ) ")] + [TestCase("[a](b ' t ' ) ")] + + [TestCase("[a](b 't' ) ")] + [TestCase("[a](b ' t' ) ")] + [TestCase("[a](b 't ' ) ")] + [TestCase("[a](b ' t ' ) ")] + + [TestCase("[a](b (t)) ")] + [TestCase("[a](b ( t)) ")] + [TestCase("[a](b (t )) ")] + [TestCase("[a](b ( t )) ")] + + [TestCase("[a](b (t)) ")] + [TestCase("[a](b ( t)) ")] + [TestCase("[a](b (t )) ")] + [TestCase("[a](b ( t )) ")] + + [TestCase("[a](b (t) ) ")] + [TestCase("[a](b ( t) ) ")] + [TestCase("[a](b (t ) ) ")] + [TestCase("[a](b ( t ) ) ")] + + [TestCase("[a](b (t) ) ")] + [TestCase("[a](b ( t) ) ")] + [TestCase("[a](b (t ) ) ")] + [TestCase("[a](b ( t ) ) ")] + public void Test_Title(string value) + { + RoundTrip(value); + } + + [TestCase("[a](<>)")] + [TestCase("[a]( <>)")] + [TestCase("[a](<> )")] + [TestCase("[a]( <> )")] + + [TestCase("[a](< >)")] + [TestCase("[a]( < >)")] + [TestCase("[a](< > )")] + [TestCase("[a]( < > )")] + + [TestCase("[a]()")] + [TestCase("[a]()")] + [TestCase("[a](< b>)")] + [TestCase("[a](< b >)")] + + [TestCase("[a]()")] + [TestCase("[a]()")] + [TestCase("[a](< b b >)")] + public void Test_PointyBrackets(string value) + { + RoundTrip(value); + } + + // | [ a ]( b " t " ) | + [TestCase(" [ a ]( b \" t \" ) ")] + [TestCase("\v[\va\v](\vb\v\"\vt\v\"\v)\v")] + [TestCase("\f[\fa\f](\fb\f\"\ft\f\"\f)\f")] + [TestCase("\t[\ta\t](\tb\t\"\tt\t\"\t)\t")] + public void Test_UncommonWhitespace(string value) { RoundTrip(value); } diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 44340ba72..1b9594430 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -4,6 +4,7 @@ Roundtrip support allows parsing of Markdown to subsequently render it back to M # Guidelines - newlines before blocks are assigned to that block - whitespace starting on a line is assigned to the block on that line +- assigning whitespace *before* a node has precedence over asigning whitespace *after* a node ## Quoteblock Quoteblocks may have different syntactical characters applied per line. That is, some lines belonging to a Quoteblock may and others **may not** contain the quote marker character `>`. Each line of a Quoteblock therefore stores the quote marker character and its surrounding whitespace. @@ -16,13 +17,14 @@ In order: - ~~`p\n p`: affects many tests~~ - ~~`\r\n` and `\r` support~~ - ~~support SetextHeading~~ -- check char.IsWhitespace() calls -- check char.IsNewline() calls -- support LinkReferenceDefinition -- support link parsing +- ~~support LinkReferenceDefinition~~ +- ~~support link parsing~~ +- support AutolinkInline - `\0` - generate spec examples as tests for roundtrip - fix `TODO: RTP: ` +- check char.IsWhitespace() calls +- check char.IsNewline() calls - introduce feature flag - extract MarkdownRenderer - cleanup NormalizeRenderer (MarkdownRenderer) @@ -39,3 +41,13 @@ In order: - document how trivia are handled generically and specifically - write tree comparison tests? - write tree visualization tool? + +# Pull request discussion +- LinkHelper duplication +- StringSlice vs String +- amount of tests + - should we create even more permutations using `\v`, `\f`? +- newlines + - Newline struct itself + - handling newlines + - should newlines be supported? diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index 7901ecb33..76291100c 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -181,6 +181,22 @@ public void TestLabel(string value) RoundTrip(value); } + [TestCase("[a]: /r ()")] + [TestCase("[a]: /r (t)")] + [TestCase("[a]: /r ( t)")] + [TestCase("[a]: /r (t )")] + [TestCase("[a]: /r ( t )")] + + [TestCase("[a]: /r ''")] + [TestCase("[a]: /r 't'")] + [TestCase("[a]: /r ' t'")] + [TestCase("[a]: /r 't '")] + [TestCase("[a]: /r ' t '")] + public void Test_Title(string value) + { + RoundTrip(value); + } + [TestCase("[a]: /r\n===\n[a]")] public void TestSetextHeader(string value) { diff --git a/src/Markdig.Tests/TestLinkHelper.cs b/src/Markdig.Tests/TestLinkHelper.cs index 7ef1f08a5..345dd3e54 100644 --- a/src/Markdig.Tests/TestLinkHelper.cs +++ b/src/Markdig.Tests/TestLinkHelper.cs @@ -15,7 +15,7 @@ public class TestLinkHelper public void TestUrlSimple() { var text = new StringSlice("toto tutu"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); Assert.AreEqual("toto", link); Assert.AreEqual(' ', text.CurrentChar); } @@ -24,7 +24,7 @@ public void TestUrlSimple() public void TestUrlUrl() { var text = new StringSlice("http://google.com)"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); Assert.AreEqual("http://google.com", link); Assert.AreEqual(')', text.CurrentChar); } @@ -35,7 +35,7 @@ public void TestUrlUrl() public void TestUrlTrailingFullStop(string uri) { var text = new StringSlice(uri); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link, true)); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _, true)); Assert.AreEqual("http://google.com", link); Assert.AreEqual('.', text.CurrentChar); } @@ -44,7 +44,7 @@ public void TestUrlTrailingFullStop(string uri) public void TestUrlNestedParenthesis() { var text = new StringSlice("(toto)tutu(tata) nooo"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); Assert.AreEqual("(toto)tutu(tata)", link); Assert.AreEqual(' ', text.CurrentChar); } @@ -53,7 +53,7 @@ public void TestUrlNestedParenthesis() public void TestUrlAlternate() { var text = new StringSlice(" nooo"); - Assert.True(LinkHelper.TryParseUrl(ref text, out string link)); + Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _)); Assert.AreEqual("toto_tata_tutu", link); Assert.AreEqual(' ', text.CurrentChar); } @@ -62,14 +62,14 @@ public void TestUrlAlternate() public void TestUrlAlternateInvalid() { var text = new StringSlice(" 0; + + c = text.CurrentChar; + if (c == ')') + { + isValid = true; + } + else if (hasWhiteSpaces) + { + c = text.CurrentChar; + pos = text.Start; + if (c == ')') + { + isValid = true; + } + else if (TryParseTitle(ref text, out title, out titleEnclosingCharacter)) + { + titleSpan.Start = pos; + titleSpan.End = text.Start - 1; + if (titleSpan.End < titleSpan.Start) + { + titleSpan = SourceSpan.Empty; + } + var startWhitespace = text.Start; + text.TrimStart(); + whitespaceAfterTitle = new SourceSpan(startWhitespace, text.Start - 1); + c = text.CurrentChar; + + if (c == ')') + { + isValid = true; + } + } + } + } + } + + if (isValid) + { + // Skip ')' + text.NextChar(); + title ??= string.Empty; + } + return isValid; + } + public static bool TryParseTitle(T text, out string title) where T : ICharIterator { - return TryParseTitle(ref text, out title); + return TryParseTitle(ref text, out title, out _); } - public static bool TryParseTitle(ref T text, out string title) where T : ICharIterator + public static bool TryParseTitle(ref T text, out string title, out char enclosingCharacter) where T : ICharIterator { bool isValid = false; var buffer = StringBuilderCache.Local(); + enclosingCharacter = '\0'; // a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or // a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or var c = text.CurrentChar; if (c == '\'' || c == '"' || c == '(') { + enclosingCharacter = c; var closingQuote = c == '(' ? ')' : c; bool hasEscape = false; // -1: undefined @@ -514,12 +614,13 @@ public static bool TryParseTitle(ref T text, out string title) where T : ICha public static bool TryParseUrl(T text, out string link) where T : ICharIterator { - return TryParseUrl(ref text, out link); + return TryParseUrl(ref text, out link, out _); } - public static bool TryParseUrl(ref T text, out string link, bool isAutoLink = false) where T : ICharIterator + public static bool TryParseUrl(ref T text, out string link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator { bool isValid = false; + hasPointyBrackets = false; var buffer = StringBuilderCache.Local(); var c = text.CurrentChar; @@ -535,6 +636,7 @@ public static bool TryParseUrl(ref T text, out string link, bool isAutoLink = if (!hasEscape && c == '>') { text.NextChar(); + hasPointyBrackets = true; isValid = true; break; } @@ -758,7 +860,7 @@ public static bool TryParseLinkReferenceDefinition(ref T text, out string lab urlSpan.Start = text.Start; bool isAngleBracketsUrl = text.CurrentChar == '<'; - if (!TryParseUrl(ref text, out url) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + if (!TryParseUrl(ref text, out url, out _) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) { return false; } @@ -770,7 +872,7 @@ public static bool TryParseLinkReferenceDefinition(ref T text, out string lab if (c == '\'' || c == '"' || c == '(') { titleSpan.Start = text.Start; - if (TryParseTitle(ref text, out title)) + if (TryParseTitle(ref text, out title, out _)) { titleSpan.End = text.Start - 1; // If we have a title, it requires a whitespace after the url @@ -826,8 +928,10 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( out string labelWithWhitespace, out SourceSpan whitespaceBeforeUrl, // can contain newline out string url, + out bool urlHasPointyBrackets, out SourceSpan whitespaceBeforeTitle, // can contain newline out string title, // can contain non-consecutive newlines + out char titleEnclosingCharacter, out SourceSpan whitespaceAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, @@ -844,6 +948,8 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( text.TrimStart(); whitespaceBeforeLabel = new SourceSpan(0, text.Start - 1); whitespaceAfterTitle = SourceSpan.Empty; + urlHasPointyBrackets = false; + titleEnclosingCharacter = '\0'; if (!TryParseLabelWhitespace(ref text, out label, out labelWithWhitespace, out labelSpan)) { @@ -864,7 +970,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( urlSpan.Start = text.Start; bool isAngleBracketsUrl = text.CurrentChar == '<'; - if (!TryParseUrl(ref text, out url) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + if (!TryParseUrl(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) { return false; } @@ -879,7 +985,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( if (c == '\'' || c == '"' || c == '(') { titleSpan.Start = text.Start; - if (TryParseTitle(ref text, out title)) + if (TryParseTitle(ref text, out title, out titleEnclosingCharacter)) { titleSpan.End = text.Start - 1; // If we have a title, it requires a whitespace after the url diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index d71bfa969..0af82ca51 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -106,6 +106,8 @@ public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool /// public TextWriter DebugLog { get; set; } + public bool TrackTrivia { get; } = true; + /// /// Gets the literal inline parser. /// diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index c65e12e1f..a6f46a4ee 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -41,6 +41,9 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) return false; } } + string label; + string labelWithWhitespace = null; + SourceSpan labelSpan = SourceSpan.Empty; switch (c) { @@ -50,11 +53,24 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var saved = slice; // If the label is followed by either a ( or a [, this is not a shortcut - if (LinkHelper.TryParseLabel(ref slice, out string label, out SourceSpan labelSpan)) + if (processor.TrackTrivia) { - if (!processor.Document.ContainsLinkReferenceDefinition(label)) + if (LinkHelper.TryParseLabelWhitespace(ref slice, out label, out labelWithWhitespace, out labelSpan)) { - label = null; + if (!processor.Document.ContainsLinkReferenceDefinition(label)) + { + label = null; + } + } + } + else + { + if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan)) + { + if (!processor.Document.ContainsLinkReferenceDefinition(label)) + { + label = null; + } } } slice = saved; @@ -65,6 +81,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { Type = DelimiterType.Open, Label = label, + LabelWithWhitespace = labelWithWhitespace, LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan), IsImage = isImage, Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)), @@ -116,6 +133,7 @@ private bool ProcessLinkReference(InlineProcessor state, string label, bool isSh Title = HtmlHelper.Unescape(linkRef.Title), Label = label, LabelSpan = labelSpan, + LabelWithWhitespace = linkRef.LabelWithWhitespace, UrlSpan = linkRef.UrlSpan, IsImage = parent.IsImage, IsShortcut = isShortcut, @@ -200,40 +218,99 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice if (text.CurrentChar == '(') { - if (LinkHelper.TryParseInlineLink(ref text, out string url, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)) + if (inlineState.TrackTrivia) { - // Inline Link - var link = new LinkInline() + if (LinkHelper.TryParseInlineLinkWhitespace( + ref text, + out string url, + out string title, + out char titleEnclosingCharacter, + out SourceSpan linkSpan, + out SourceSpan titleSpan, + out SourceSpan whitespaceBeforeLink, + out SourceSpan whitespaceAfterLink, + out SourceSpan whitespaceAfterTitle, + out bool urlHasPointyBrackets)) { - Url = HtmlHelper.Unescape(url), - Title = HtmlHelper.Unescape(title), - IsImage = openParent.IsImage, - LabelSpan = openParent.LabelSpan, - UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), - TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan), - Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)), - Line = openParent.Line, - Column = openParent.Column, - }; - - openParent.ReplaceBy(link); - // Notifies processor as we are creating an inline locally - inlineState.Inline = link; + var wsBeforeLink = new StringSlice(text.Text, whitespaceBeforeLink.Start, whitespaceBeforeLink.End); + var wsAfterLink = new StringSlice(text.Text, whitespaceAfterLink.Start, whitespaceAfterLink.End); + var wsAfterTitle = new StringSlice(text.Text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + // Inline Link + var link = new LinkInline() + { + WhitespaceBeforeUrl = wsBeforeLink, + Url = HtmlHelper.Unescape(url), // TODO: RTP: unescape + UrlHasPointyBrackets = urlHasPointyBrackets, + WhitespaceAfterUrl = wsAfterLink, + Title = HtmlHelper.Unescape(title), // TODO: RTP: unescape + TitleEnclosingCharacter = titleEnclosingCharacter, + WhitespaceAfterTitle = wsAfterTitle, + IsImage = openParent.IsImage, + LabelSpan = openParent.LabelSpan, + UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), + TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan), + Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)), + Line = openParent.Line, + Column = openParent.Column, + }; + + openParent.ReplaceBy(link); + // Notifies processor as we are creating an inline locally + inlineState.Inline = link; + + // Process emphasis delimiters + inlineState.PostProcessInlines(0, link, null, false); + + // If we have a link (and not an image), + // we also set all [ delimiters before the opening delimiter to inactive. + // (This will prevent us from getting links within links.) + if (!openParent.IsImage) + { + MarkParentAsInactive(parentDelimiter); + } - // Process emphasis delimiters - inlineState.PostProcessInlines(0, link, null, false); + link.IsClosed = true; - // If we have a link (and not an image), - // we also set all [ delimiters before the opening delimiter to inactive. - // (This will prevent us from getting links within links.) - if (!openParent.IsImage) - { - MarkParentAsInactive(parentDelimiter); + return true; } + } + else + { + if (LinkHelper.TryParseInlineLink(ref text, out string url, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)) + { + // Inline Link + var link = new LinkInline() + { + Url = HtmlHelper.Unescape(url), + Title = HtmlHelper.Unescape(title), + IsImage = openParent.IsImage, + LabelSpan = openParent.LabelSpan, + UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), + TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan), + Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)), + Line = openParent.Line, + Column = openParent.Column, + }; + + openParent.ReplaceBy(link); + // Notifies processor as we are creating an inline locally + inlineState.Inline = link; + + // Process emphasis delimiters + inlineState.PostProcessInlines(0, link, null, false); + + // If we have a link (and not an image), + // we also set all [ delimiters before the opening delimiter to inactive. + // (This will prevent us from getting links within links.) + if (!openParent.IsImage) + { + MarkParentAsInactive(parentDelimiter); + } - link.IsClosed = true; + link.IsClosed = true; - return true; + return true; + } } text = savedText; diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index 9502f9729..723a3dced 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -32,7 +32,9 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) if (link.Label != null) { - if (link.FirstChild is LiteralInline literal && literal.Content.Length == link.Label.Length && literal.Content.Match(link.Label)) + if (link.FirstChild is LiteralInline literal && + literal.Content.Length == link.Label.Length && + literal.Content.Match(link.Label)) { // collapsed reference and shortcut links if (!link.IsShortcut) @@ -43,20 +45,38 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) else { // full link - renderer.Write('[').Write(link.Label).Write(']'); + renderer.Write('[').Write(link.LabelWithWhitespace).Write(']'); } } else { if (link.Url != null) { - renderer.Write('(').Write(link.Url); + renderer.Write('('); + renderer.Write(link.WhitespaceBeforeUrl); + if (link.UrlHasPointyBrackets) + { + renderer.Write('<'); + } + renderer.Write(link.Url); + if (link.UrlHasPointyBrackets) + { + renderer.Write('>'); + } + renderer.Write(link.WhitespaceAfterUrl); if (!string.IsNullOrEmpty(link.Title)) { - renderer.Write(" \""); - renderer.Write(link.Title.Replace(@"""", @"\""")); - renderer.Write("\""); + var open = link.TitleEnclosingCharacter; + var close = link.TitleEnclosingCharacter; + if (link.TitleEnclosingCharacter == '(') + { + close = ')'; + } + renderer.Write(open); + renderer.Write(link.Title.Replace(@"""", @"\""")); // TODO: RTP: should this always be done? + renderer.Write(close); + renderer.Write(link.WhitespaceAfterTitle); } renderer.Write(')'); diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs index f6fa5da22..1c09b50a2 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs @@ -23,9 +23,15 @@ protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitio renderer.Write(linkDef.WhitespaceBeforeTitle); if (linkDef.Title != null) { - renderer.Write("\""); - renderer.Write(linkDef.Title.Replace("\"", "\\\"")); - renderer.Write('"'); + var open = linkDef.TitleEnclosingCharacter; + var close = linkDef.TitleEnclosingCharacter; + if (linkDef.TitleEnclosingCharacter == '(') + { + close = ')'; + } + renderer.Write(open); + renderer.Write(linkDef.Title.Replace("\"", "\\\"")); // TODO: RTP: should this always be done? + renderer.Write(close); } renderer.Write(linkDef.AfterWhitespace); diff --git a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs index 8746eb801..8ebaf4694 100644 --- a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs @@ -26,6 +26,8 @@ public LinkDelimiterInline(InlineParser parser) : base(parser) /// public string Label { get; set; } + public string LabelWithWhitespace { get; set; } + /// /// The label span /// diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index 7c3a9e1f6..d2ae3828f 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using System.Diagnostics; namespace Markdig.Syntax.Inlines @@ -38,11 +39,17 @@ public LinkInline(string url, string title) Title = title; } + public StringSlice WhitespaceBeforeUrl { get; set; } + /// /// Gets or sets the URL. /// public string Url { get; set; } + public bool UrlHasPointyBrackets { get; set; } + + public StringSlice WhitespaceAfterUrl { get; set; } + /// /// Gets or sets the GetDynamicUrl delegate. If this property is set, /// it is used instead of to get the Url from this instance. @@ -54,11 +61,17 @@ public LinkInline(string url, string title) /// public string Title { get; set; } + public char TitleEnclosingCharacter { get; set; } // TODO: RTP: implement + + public StringSlice WhitespaceAfterTitle { get; set; } + /// /// Gets or sets the label. /// public string Label { get; set; } + public string LabelWithWhitespace { get; set; } + /// /// Gets or sets a value indicating whether this instance is an image link. /// diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index b0ae6ba09..9b430934d 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -62,6 +62,8 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// public string Url { get; set; } + public bool UrlHasPointyBrackets { get; set; } + public StringSlice WhitespaceBeforeTitle { get; set; } /// @@ -69,6 +71,8 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// public string Title { get; set; } + public char TitleEnclosingCharacter { get; set; } + /// /// The label span /// @@ -147,8 +151,10 @@ public static bool TryParseWhitespace( out string labelWithWhitespace, out whitespaceBeforeUrl, out string url, + out bool urlHasPointyBrackets, out whitespaceBeforeTitle, out string title, + out char titleEnclosingCharacter, out whitespaceAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, @@ -159,6 +165,8 @@ public static bool TryParseWhitespace( block = new LinkReferenceDefinition(label, url, title) { + UrlHasPointyBrackets = urlHasPointyBrackets, + TitleEnclosingCharacter = titleEnclosingCharacter, LabelWithWhitespace = labelWithWhitespace, LabelSpan = labelSpan, UrlSpan = urlSpan, From 47b3ac5d99c348551191ab935eeba0ed5f36b574 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 17 Oct 2020 23:19:44 +0200 Subject: [PATCH 041/120] fix Autlink tests --- .../Inlines/TestAutoLinkInline.cs | 19 ++++--------------- src/Markdig/Syntax/Inlines/LinkInline.cs | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs index a38485e38..598cd6c8d 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs @@ -10,21 +10,10 @@ public class TestAutoLinkInline [TestCase(" ")] [TestCase(" ")] [TestCase(" ")] - - [TestCase("< http://a>")] - [TestCase(" < http://a>")] - [TestCase("< http://a> ")] - [TestCase(" < http://a> ")] - - [TestCase("")] - [TestCase(" ")] - [TestCase(" ")] - [TestCase(" ")] - - [TestCase("< http://a >")] - [TestCase(" < http://a >")] - [TestCase("< http://a > ")] - [TestCase(" < http://a > ")] + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index d2ae3828f..fbce61640 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -61,7 +61,7 @@ public LinkInline(string url, string title) /// public string Title { get; set; } - public char TitleEnclosingCharacter { get; set; } // TODO: RTP: implement + public char TitleEnclosingCharacter { get; set; } public StringSlice WhitespaceAfterTitle { get; set; } From c711201b345daf10d1c48a3514d0f9a045fe5cee Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 17 Oct 2020 23:49:44 +0200 Subject: [PATCH 042/120] implement codegen for roundtrip parsing --- .../RoundtripSpecs/CommonMark.generated.cs | 15267 ++++++++++++++++ .../RoundtripSpecs/CommonMark.md | 9710 ++++++++++ .../Inlines/TestAutoLinkInline.cs | 2 +- .../Inlines/TestBackslashEscapeInline.cs | 2 +- .../RoundtripSpecs/Inlines/TestCodeInline.cs | 2 +- .../Inlines/TestEmphasisInline.cs | 2 +- .../Inlines/TestHtmlEntityInline.cs | 2 +- .../RoundtripSpecs/Inlines/TestHtmlInline.cs | 2 +- .../RoundtripSpecs/Inlines/TestImageInline.cs | 2 +- .../Inlines/TestLineBreakInline.cs | 2 +- .../RoundtripSpecs/Inlines/TestLinkInline.cs | 2 +- .../Inlines/TestNullCharacterInline.cs | 2 +- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 6 +- .../RoundtripSpecs/TestAtxHeading.cs | 2 +- .../RoundtripSpecs/TestFencedCodeBlock.cs | 2 +- .../RoundtripSpecs/TestHelper.cs | 11 - .../RoundtripSpecs/TestHtmlBlock.cs | 2 +- .../RoundtripSpecs/TestIndentedCodeBlock.cs | 2 +- .../TestLinkReferenceDefinition.cs | 2 +- .../RoundtripSpecs/TestOrderedList.cs | 2 +- .../RoundtripSpecs/TestParagraph.cs | 2 +- .../RoundtripSpecs/TestQuoteBlock.cs | 2 +- .../RoundtripSpecs/TestSetextHeading.cs | 2 +- .../RoundtripSpecs/TestThematicBreak.cs | 2 +- .../RoundtripSpecs/TestUnorderedList.cs | 2 +- src/Markdig.Tests/TestRoundtrip.cs | 28 + src/SpecFileGen/Program.cs | 13 +- 27 files changed, 25041 insertions(+), 36 deletions(-) create mode 100644 src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs create mode 100644 src/Markdig.Tests/RoundtripSpecs/CommonMark.md create mode 100644 src/Markdig.Tests/TestRoundtrip.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs b/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs new file mode 100644 index 000000000..5a274f1a5 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs @@ -0,0 +1,15267 @@ +// Generated: 2020-10-17 21:45:36 + +// -------------------------------- +// Roundtrip +// -------------------------------- + +using System; +using NUnit.Framework; + +namespace Markdig.Tests.Specs.Roundtrip.Roundtrip +{ + [TestFixture] + public class TestPreliminariesTabs + { + // --- + // title: CommonMark Spec + // author: John MacFarlane + // version: 0.29 + // date: '2019-04-06' + // license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)' + // ... + // + // # Introduction + // + // ## What is Markdown? + // + // Markdown is a plain text format for writing structured documents, + // based on conventions for indicating formatting in email + // and usenet posts. It was developed by John Gruber (with + // help from Aaron Swartz) and released in 2004 in the form of a + // [syntax description](http://daringfireball.net/projects/markdown/syntax) + // and a Perl script (`Markdown.pl`) for converting Markdown to + // HTML. In the next decade, dozens of implementations were + // developed in many languages. Some extended the original + // Markdown syntax with conventions for footnotes, tables, and + // other document elements. Some allowed Markdown documents to be + // rendered in formats other than HTML. Websites like Reddit, + // StackOverflow, and GitHub had millions of people using Markdown. + // And Markdown started to be used beyond the web, to author books, + // articles, slide shows, letters, and lecture notes. + // + // What distinguishes Markdown from many other lightweight markup + // syntaxes, which are often easier to write, is its readability. + // As Gruber writes: + // + // > The overriding design goal for Markdown's formatting syntax is + // > to make it as readable as possible. The idea is that a + // > Markdown-formatted document should be publishable as-is, as + // > plain text, without looking like it's been marked up with tags + // > or formatting instructions. + // > () + // + // The point can be illustrated by comparing a sample of + // [AsciiDoc](http://www.methods.co.nz/asciidoc/) with + // an equivalent sample of Markdown. Here is a sample of + // AsciiDoc from the AsciiDoc manual: + // + // ``` + // 1. List item one. + // + + // List item one continued with a second paragraph followed by an + // Indented block. + // + + // ................. + // $ ls *.sh + // $ mv *.sh ~/tmp + // ................. + // + + // List item continued with a third paragraph. + // + // 2. List item two continued with an open block. + // + + // -- + // This paragraph is part of the preceding list item. + // + // a. This list is nested and does not require explicit item + // continuation. + // + + // This paragraph is part of the preceding list item. + // + // b. List item b. + // + // This paragraph belongs to item two of the outer list. + // -- + // ``` + // + // And here is the equivalent in Markdown: + // ``` + // 1. List item one. + // + // List item one continued with a second paragraph followed by an + // Indented block. + // + // $ ls *.sh + // $ mv *.sh ~/tmp + // + // List item continued with a third paragraph. + // + // 2. List item two continued with an open block. + // + // This paragraph is part of the preceding list item. + // + // 1. This list is nested and does not require explicit item continuation. + // + // This paragraph is part of the preceding list item. + // + // 2. List item b. + // + // This paragraph belongs to item two of the outer list. + // ``` + // + // The AsciiDoc version is, arguably, easier to write. You don't need + // to worry about indentation. But the Markdown version is much easier + // to read. The nesting of list items is apparent to the eye in the + // source, not just in the processed document. + // + // ## Why is a spec needed? + // + // John Gruber's [canonical description of Markdown's + // syntax](http://daringfireball.net/projects/markdown/syntax) + // does not specify the syntax unambiguously. Here are some examples of + // questions it does not answer: + // + // 1. How much indentation is needed for a sublist? The spec says that + // continuation paragraphs need to be indented four spaces, but is + // not fully explicit about sublists. It is natural to think that + // they, too, must be indented four spaces, but `Markdown.pl` does + // not require that. This is hardly a "corner case," and divergences + // between implementations on this issue often lead to surprises for + // users in real documents. (See [this comment by John + // Gruber](http://article.gmane.org/gmane.text.markdown.general/1997).) + // + // 2. Is a blank line needed before a block quote or heading? + // Most implementations do not require the blank line. However, + // this can lead to unexpected results in hard-wrapped text, and + // also to ambiguities in parsing (note that some implementations + // put the heading inside the blockquote, while others do not). + // (John Gruber has also spoken [in favor of requiring the blank + // lines](http://article.gmane.org/gmane.text.markdown.general/2146).) + // + // 3. Is a blank line needed before an indented code block? + // (`Markdown.pl` requires it, but this is not mentioned in the + // documentation, and some implementations do not require it.) + // + // ``` markdown + // paragraph + // code? + // ``` + // + // 4. What is the exact rule for determining when list items get + // wrapped in `

` tags? Can a list be partially "loose" and partially + // "tight"? What should we do with a list like this? + // + // ``` markdown + // 1. one + // + // 2. two + // 3. three + // ``` + // + // Or this? + // + // ``` markdown + // 1. one + // - a + // + // - b + // 2. two + // ``` + // + // (There are some relevant comments by John Gruber + // [here](http://article.gmane.org/gmane.text.markdown.general/2554).) + // + // 5. Can list markers be indented? Can ordered list markers be right-aligned? + // + // ``` markdown + // 8. item 1 + // 9. item 2 + // 10. item 2a + // ``` + // + // 6. Is this one list with a thematic break in its second item, + // or two lists separated by a thematic break? + // + // ``` markdown + // * a + // * * * * * + // * b + // ``` + // + // 7. When list markers change from numbers to bullets, do we have + // two lists or one? (The Markdown syntax description suggests two, + // but the perl scripts and many other implementations produce one.) + // + // ``` markdown + // 1. fee + // 2. fie + // - foe + // - fum + // ``` + // + // 8. What are the precedence rules for the markers of inline structure? + // For example, is the following a valid link, or does the code span + // take precedence ? + // + // ``` markdown + // [a backtick (`)](/url) and [another backtick (`)](/url). + // ``` + // + // 9. What are the precedence rules for markers of emphasis and strong + // emphasis? For example, how should the following be parsed? + // + // ``` markdown + // *foo *bar* baz* + // ``` + // + // 10. What are the precedence rules between block-level and inline-level + // structure? For example, how should the following be parsed? + // + // ``` markdown + // - `a long code span can contain a hyphen like this + // - and it can screw things up` + // ``` + // + // 11. Can list items include section headings? (`Markdown.pl` does not + // allow this, but does allow blockquotes to include headings.) + // + // ``` markdown + // - # Heading + // ``` + // + // 12. Can list items be empty? + // + // ``` markdown + // * a + // * + // * b + // ``` + // + // 13. Can link references be defined inside block quotes or list items? + // + // ``` markdown + // > Blockquote [foo]. + // > + // > [foo]: /url + // ``` + // + // 14. If there are multiple definitions for the same reference, which takes + // precedence? + // + // ``` markdown + // [foo]: /url1 + // [foo]: /url2 + // + // [foo][] + // ``` + // + // In the absence of a spec, early implementers consulted `Markdown.pl` + // to resolve these ambiguities. But `Markdown.pl` was quite buggy, and + // gave manifestly bad results in many cases, so it was not a + // satisfactory replacement for a spec. + // + // Because there is no unambiguous spec, implementations have diverged + // considerably. As a result, users are often surprised to find that + // a document that renders one way on one system (say, a GitHub wiki) + // renders differently on another (say, converting to docbook using + // pandoc). To make matters worse, because nothing in Markdown counts + // as a "syntax error," the divergence often isn't discovered right away. + // + // ## About this document + // + // This document attempts to specify Markdown syntax unambiguously. + // It contains many examples with side-by-side Markdown and + // HTML. These are intended to double as conformance tests. An + // accompanying script `spec_tests.py` can be used to run the tests + // against any Markdown program: + // + // python test/spec_tests.py --spec spec.txt --program PROGRAM + // + // Since this document describes how Markdown is to be parsed into + // an abstract syntax tree, it would have made sense to use an abstract + // representation of the syntax tree instead of HTML. But HTML is capable + // of representing the structural distinctions we need to make, and the + // choice of HTML for the tests makes it possible to run the tests against + // an implementation without writing an abstract syntax tree renderer. + // + // This document is generated from a text file, `spec.txt`, written + // in Markdown with a small extension for the side-by-side tests. + // The script `tools/makespec.py` can be used to convert `spec.txt` into + // HTML or CommonMark (which can then be converted into other formats). + // + // In the examples, the `→` character is used to represent tabs. + // + // # Preliminaries + // + // ## Characters and lines + // + // Any sequence of [characters] is a valid CommonMark + // document. + // + // A [character](@) is a Unicode code point. Although some + // code points (for example, combining accents) do not correspond to + // characters in an intuitive sense, all code points count as characters + // for purposes of this spec. + // + // This spec does not specify an encoding; it thinks of lines as composed + // of [characters] rather than bytes. A conforming parser may be limited + // to a certain encoding. + // + // A [line](@) is a sequence of zero or more [characters] + // other than newline (`U+000A`) or carriage return (`U+000D`), + // followed by a [line ending] or by the end of file. + // + // A [line ending](@) is a newline (`U+000A`), a carriage return + // (`U+000D`) not followed by a newline, or a carriage return and a + // following newline. + // + // A line containing no characters, or a line containing only spaces + // (`U+0020`) or tabs (`U+0009`), is called a [blank line](@). + // + // The following definitions of character classes will be used in this spec: + // + // A [whitespace character](@) is a space + // (`U+0020`), tab (`U+0009`), newline (`U+000A`), line tabulation (`U+000B`), + // form feed (`U+000C`), or carriage return (`U+000D`). + // + // [Whitespace](@) is a sequence of one or more [whitespace + // characters]. + // + // A [Unicode whitespace character](@) is + // any code point in the Unicode `Zs` general category, or a tab (`U+0009`), + // carriage return (`U+000D`), newline (`U+000A`), or form feed + // (`U+000C`). + // + // [Unicode whitespace](@) is a sequence of one + // or more [Unicode whitespace characters]. + // + // A [space](@) is `U+0020`. + // + // A [non-whitespace character](@) is any character + // that is not a [whitespace character]. + // + // An [ASCII punctuation character](@) + // is `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, + // `*`, `+`, `,`, `-`, `.`, `/` (U+0021–2F), + // `:`, `;`, `<`, `=`, `>`, `?`, `@` (U+003A–0040), + // `[`, `\`, `]`, `^`, `_`, `` ` `` (U+005B–0060), + // `{`, `|`, `}`, or `~` (U+007B–007E). + // + // A [punctuation character](@) is an [ASCII + // punctuation character] or anything in + // the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or `Ps`. + // + // ## Tabs + // + // Tabs in lines are not expanded to [spaces]. However, + // in contexts where whitespace helps to define block structure, + // tabs behave as if they were replaced by spaces with a tab stop + // of 4 characters. + // + // Thus, for example, a tab can be used instead of four spaces + // in an indented code block. (Note, however, that internal + // tabs are passed through as literal tabs, not expanded to + // spaces.) + [Test] + public void PreliminariesTabs_Example001() + { + // Example 1 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // →foo→baz→→bim + // + // Should be rendered as: + //

foo→baz→→bim
+            //     
+ + Console.WriteLine("Example 1\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec("\tfoo\tbaz\t\tbim", "
foo\tbaz\t\tbim\n
", ""); + } + + [Test] + public void PreliminariesTabs_Example002() + { + // Example 2 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // →foo→baz→→bim + // + // Should be rendered as: + //
foo→baz→→bim
+            //     
+ + Console.WriteLine("Example 2\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec(" \tfoo\tbaz\t\tbim", "
foo\tbaz\t\tbim\n
", ""); + } + + [Test] + public void PreliminariesTabs_Example003() + { + // Example 3 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // a→a + // ὐ→a + // + // Should be rendered as: + //
a→a
+            //     ὐ→a
+            //     
+ + Console.WriteLine("Example 3\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec(" a\ta\n ὐ\ta", "
a\ta\nὐ\ta\n
", ""); + } + + // In the following example, a continuation paragraph of a list + // item is indented with a tab; this has exactly the same effect + // as indentation with four spaces would: + [Test] + public void PreliminariesTabs_Example004() + { + // Example 4 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // - foo + // + // →bar + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //

    bar

    + //
  • + //
+ + Console.WriteLine("Example 4\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec(" - foo\n\n\tbar", "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
", ""); + } + + [Test] + public void PreliminariesTabs_Example005() + { + // Example 5 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // - foo + // + // →→bar + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //
      bar
    +            //     
    + //
  • + //
+ + Console.WriteLine("Example 5\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec("- foo\n\n\t\tbar", "
    \n
  • \n

    foo

    \n
      bar\n
    \n
  • \n
", ""); + } + + // Normally the `>` that begins a block quote may be followed + // optionally by a space, which is not considered part of the + // content. In the following case `>` is followed by a tab, + // which is treated as if it were expanded into three spaces. + // Since one of these spaces is considered part of the + // delimiter, `foo` is considered to be indented six spaces + // inside the block quote context, so we get an indented + // code block starting with two spaces. + [Test] + public void PreliminariesTabs_Example006() + { + // Example 6 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // >→→foo + // + // Should be rendered as: + //
+ //
  foo
+            //     
+ //
+ + Console.WriteLine("Example 6\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec(">\t\tfoo", "
\n
  foo\n
\n
", ""); + } + + [Test] + public void PreliminariesTabs_Example007() + { + // Example 7 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // -→→foo + // + // Should be rendered as: + //
    + //
  • + //
      foo
    +            //     
    + //
  • + //
+ + Console.WriteLine("Example 7\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec("-\t\tfoo", "
    \n
  • \n
      foo\n
    \n
  • \n
", ""); + } + + [Test] + public void PreliminariesTabs_Example008() + { + // Example 8 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // foo + // →bar + // + // Should be rendered as: + //
foo
+            //     bar
+            //     
+ + Console.WriteLine("Example 8\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec(" foo\n\tbar", "
foo\nbar\n
", ""); + } + + [Test] + public void PreliminariesTabs_Example009() + { + // Example 9 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // - foo + // - bar + // → - baz + // + // Should be rendered as: + //
    + //
  • foo + //
      + //
    • bar + //
        + //
      • baz
      • + //
      + //
    • + //
    + //
  • + //
+ + Console.WriteLine("Example 9\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec(" - foo\n - bar\n\t - baz", "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz
      • \n
      \n
    • \n
    \n
  • \n
", ""); + } + + [Test] + public void PreliminariesTabs_Example010() + { + // Example 10 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // #→Foo + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 10\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec("#\tFoo", "

Foo

", ""); + } + + [Test] + public void PreliminariesTabs_Example011() + { + // Example 11 + // Section: Preliminaries / Tabs + // + // The following Markdown: + // *→*→*→ + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 11\nSection Preliminaries / Tabs\n"); + TestRoundtrip.TestSpec("*\t*\t*\t", "
", ""); + } + } + + [TestFixture] + public class TestBlocksAndInlinesPrecedence + { + // ## Insecure characters + // + // For security reasons, the Unicode character `U+0000` must be replaced + // with the REPLACEMENT CHARACTER (`U+FFFD`). + // + // # Blocks and inlines + // + // We can think of a document as a sequence of + // [blocks](@)---structural elements like paragraphs, block + // quotations, lists, headings, rules, and code blocks. Some blocks (like + // block quotes and list items) contain other blocks; others (like + // headings and paragraphs) contain [inline](@) content---text, + // links, emphasized text, images, code spans, and so on. + // + // ## Precedence + // + // Indicators of block structure always take precedence over indicators + // of inline structure. So, for example, the following is a list with + // two items, not a list with one item containing a code span: + [Test] + public void BlocksAndInlinesPrecedence_Example012() + { + // Example 12 + // Section: Blocks and inlines / Precedence + // + // The following Markdown: + // - `one + // - two` + // + // Should be rendered as: + //
    + //
  • `one
  • + //
  • two`
  • + //
+ + Console.WriteLine("Example 12\nSection Blocks and inlines / Precedence\n"); + TestRoundtrip.TestSpec("- `one\n- two`", "
    \n
  • `one
  • \n
  • two`
  • \n
", ""); + } + } + + [TestFixture] + public class TestLeafBlocksThematicBreaks + { + // This means that parsing can proceed in two steps: first, the block + // structure of the document can be discerned; second, text lines inside + // paragraphs, headings, and other block constructs can be parsed for inline + // structure. The second step requires information about link reference + // definitions that will be available only at the end of the first + // step. Note that the first step requires processing lines in sequence, + // but the second can be parallelized, since the inline parsing of + // one block element does not affect the inline parsing of any other. + // + // ## Container blocks and leaf blocks + // + // We can divide blocks into two types: + // [container blocks](@), + // which can contain other blocks, and [leaf blocks](@), + // which cannot. + // + // # Leaf blocks + // + // This section describes the different kinds of leaf block that make up a + // Markdown document. + // + // ## Thematic breaks + // + // A line consisting of 0-3 spaces of indentation, followed by a sequence + // of three or more matching `-`, `_`, or `*` characters, each followed + // optionally by any number of spaces or tabs, forms a + // [thematic break](@). + [Test] + public void LeafBlocksThematicBreaks_Example013() + { + // Example 13 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // *** + // --- + // ___ + // + // Should be rendered as: + //
+ //
+ //
+ + Console.WriteLine("Example 13\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("***\n---\n___", "
\n
\n
", ""); + } + + // Wrong characters: + [Test] + public void LeafBlocksThematicBreaks_Example014() + { + // Example 14 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // +++ + // + // Should be rendered as: + //

+++

+ + Console.WriteLine("Example 14\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("+++", "

+++

", ""); + } + + [Test] + public void LeafBlocksThematicBreaks_Example015() + { + // Example 15 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // === + // + // Should be rendered as: + //

===

+ + Console.WriteLine("Example 15\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("===", "

===

", ""); + } + + // Not enough characters: + [Test] + public void LeafBlocksThematicBreaks_Example016() + { + // Example 16 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // -- + // ** + // __ + // + // Should be rendered as: + //

-- + // ** + // __

+ + Console.WriteLine("Example 16\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("--\n**\n__", "

--\n**\n__

", ""); + } + + // One to three spaces indent are allowed: + [Test] + public void LeafBlocksThematicBreaks_Example017() + { + // Example 17 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // *** + // *** + // *** + // + // Should be rendered as: + //
+ //
+ //
+ + Console.WriteLine("Example 17\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec(" ***\n ***\n ***", "
\n
\n
", ""); + } + + // Four spaces is too many: + [Test] + public void LeafBlocksThematicBreaks_Example018() + { + // Example 18 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // *** + // + // Should be rendered as: + //
***
+            //     
+ + Console.WriteLine("Example 18\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec(" ***", "
***\n
", ""); + } + + [Test] + public void LeafBlocksThematicBreaks_Example019() + { + // Example 19 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // Foo + // *** + // + // Should be rendered as: + //

Foo + // ***

+ + Console.WriteLine("Example 19\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("Foo\n ***", "

Foo\n***

", ""); + } + + // More than three characters may be used: + [Test] + public void LeafBlocksThematicBreaks_Example020() + { + // Example 20 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // _____________________________________ + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 20\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("_____________________________________", "
", ""); + } + + // Spaces are allowed between the characters: + [Test] + public void LeafBlocksThematicBreaks_Example021() + { + // Example 21 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // - - - + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 21\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec(" - - -", "
", ""); + } + + [Test] + public void LeafBlocksThematicBreaks_Example022() + { + // Example 22 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // ** * ** * ** * ** + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 22\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec(" ** * ** * ** * **", "
", ""); + } + + [Test] + public void LeafBlocksThematicBreaks_Example023() + { + // Example 23 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // - - - - + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 23\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("- - - -", "
", ""); + } + + // Spaces are allowed at the end: + [Test] + public void LeafBlocksThematicBreaks_Example024() + { + // Example 24 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // - - - - + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 24\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("- - - - ", "
", ""); + } + + // However, no other characters may occur in the line: + [Test] + public void LeafBlocksThematicBreaks_Example025() + { + // Example 25 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // _ _ _ _ a + // + // a------ + // + // ---a--- + // + // Should be rendered as: + //

_ _ _ _ a

+ //

a------

+ //

---a---

+ + Console.WriteLine("Example 25\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("_ _ _ _ a\n\na------\n\n---a---", "

_ _ _ _ a

\n

a------

\n

---a---

", ""); + } + + // It is required that all of the [non-whitespace characters] be the same. + // So, this is not a thematic break: + [Test] + public void LeafBlocksThematicBreaks_Example026() + { + // Example 26 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // *-* + // + // Should be rendered as: + //

-

+ + Console.WriteLine("Example 26\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec(" *-*", "

-

", ""); + } + + // Thematic breaks do not need blank lines before or after: + [Test] + public void LeafBlocksThematicBreaks_Example027() + { + // Example 27 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // - foo + // *** + // - bar + // + // Should be rendered as: + //
    + //
  • foo
  • + //
+ //
+ //
    + //
  • bar
  • + //
+ + Console.WriteLine("Example 27\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("- foo\n***\n- bar", "
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
", ""); + } + + // Thematic breaks can interrupt a paragraph: + [Test] + public void LeafBlocksThematicBreaks_Example028() + { + // Example 28 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // Foo + // *** + // bar + // + // Should be rendered as: + //

Foo

+ //
+ //

bar

+ + Console.WriteLine("Example 28\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("Foo\n***\nbar", "

Foo

\n
\n

bar

", ""); + } + + // If a line of dashes that meets the above conditions for being a + // thematic break could also be interpreted as the underline of a [setext + // heading], the interpretation as a + // [setext heading] takes precedence. Thus, for example, + // this is a setext heading, not a paragraph followed by a thematic break: + [Test] + public void LeafBlocksThematicBreaks_Example029() + { + // Example 29 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // Foo + // --- + // bar + // + // Should be rendered as: + //

Foo

+ //

bar

+ + Console.WriteLine("Example 29\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("Foo\n---\nbar", "

Foo

\n

bar

", ""); + } + + // When both a thematic break and a list item are possible + // interpretations of a line, the thematic break takes precedence: + [Test] + public void LeafBlocksThematicBreaks_Example030() + { + // Example 30 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // * Foo + // * * * + // * Bar + // + // Should be rendered as: + //
    + //
  • Foo
  • + //
+ //
+ //
    + //
  • Bar
  • + //
+ + Console.WriteLine("Example 30\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("* Foo\n* * *\n* Bar", "
    \n
  • Foo
  • \n
\n
\n
    \n
  • Bar
  • \n
", ""); + } + + // If you want a thematic break in a list item, use a different bullet: + [Test] + public void LeafBlocksThematicBreaks_Example031() + { + // Example 31 + // Section: Leaf blocks / Thematic breaks + // + // The following Markdown: + // - Foo + // - * * * + // + // Should be rendered as: + //
    + //
  • Foo
  • + //
  • + //
    + //
  • + //
+ + Console.WriteLine("Example 31\nSection Leaf blocks / Thematic breaks\n"); + TestRoundtrip.TestSpec("- Foo\n- * * *", "
    \n
  • Foo
  • \n
  • \n
    \n
  • \n
", ""); + } + } + + [TestFixture] + public class TestLeafBlocksATXHeadings + { + // ## ATX headings + // + // An [ATX heading](@) + // consists of a string of characters, parsed as inline content, between an + // opening sequence of 1--6 unescaped `#` characters and an optional + // closing sequence of any number of unescaped `#` characters. + // The opening sequence of `#` characters must be followed by a + // [space] or by the end of line. The optional closing sequence of `#`s must be + // preceded by a [space] and may be followed by spaces only. The opening + // `#` character may be indented 0-3 spaces. The raw contents of the + // heading are stripped of leading and trailing spaces before being parsed + // as inline content. The heading level is equal to the number of `#` + // characters in the opening sequence. + // + // Simple headings: + [Test] + public void LeafBlocksATXHeadings_Example032() + { + // Example 32 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // # foo + // ## foo + // ### foo + // #### foo + // ##### foo + // ###### foo + // + // Should be rendered as: + //

foo

+ //

foo

+ //

foo

+ //

foo

+ //
foo
+ //
foo
+ + Console.WriteLine("Example 32\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo", "

foo

\n

foo

\n

foo

\n

foo

\n
foo
\n
foo
", ""); + } + + // More than six `#` characters is not a heading: + [Test] + public void LeafBlocksATXHeadings_Example033() + { + // Example 33 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ####### foo + // + // Should be rendered as: + //

####### foo

+ + Console.WriteLine("Example 33\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("####### foo", "

####### foo

", ""); + } + + // At least one space is required between the `#` characters and the + // heading's contents, unless the heading is empty. Note that many + // implementations currently do not require the space. However, the + // space was required by the + // [original ATX implementation](http://www.aaronsw.com/2002/atx/atx.py), + // and it helps prevent things like the following from being parsed as + // headings: + [Test] + public void LeafBlocksATXHeadings_Example034() + { + // Example 34 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // #5 bolt + // + // #hashtag + // + // Should be rendered as: + //

#5 bolt

+ //

#hashtag

+ + Console.WriteLine("Example 34\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("#5 bolt\n\n#hashtag", "

#5 bolt

\n

#hashtag

", ""); + } + + // This is not a heading, because the first `#` is escaped: + [Test] + public void LeafBlocksATXHeadings_Example035() + { + // Example 35 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // \## foo + // + // Should be rendered as: + //

## foo

+ + Console.WriteLine("Example 35\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("\\## foo", "

## foo

", ""); + } + + // Contents are parsed as inlines: + [Test] + public void LeafBlocksATXHeadings_Example036() + { + // Example 36 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // # foo *bar* \*baz\* + // + // Should be rendered as: + //

foo bar *baz*

+ + Console.WriteLine("Example 36\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("# foo *bar* \\*baz\\*", "

foo bar *baz*

", ""); + } + + // Leading and trailing [whitespace] is ignored in parsing inline content: + [Test] + public void LeafBlocksATXHeadings_Example037() + { + // Example 37 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // # foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 37\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("# foo ", "

foo

", ""); + } + + // One to three spaces indentation are allowed: + [Test] + public void LeafBlocksATXHeadings_Example038() + { + // Example 38 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ### foo + // ## foo + // # foo + // + // Should be rendered as: + //

foo

+ //

foo

+ //

foo

+ + Console.WriteLine("Example 38\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec(" ### foo\n ## foo\n # foo", "

foo

\n

foo

\n

foo

", ""); + } + + // Four spaces are too much: + [Test] + public void LeafBlocksATXHeadings_Example039() + { + // Example 39 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // # foo + // + // Should be rendered as: + //
# foo
+            //     
+ + Console.WriteLine("Example 39\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec(" # foo", "
# foo\n
", ""); + } + + [Test] + public void LeafBlocksATXHeadings_Example040() + { + // Example 40 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // foo + // # bar + // + // Should be rendered as: + //

foo + // # bar

+ + Console.WriteLine("Example 40\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("foo\n # bar", "

foo\n# bar

", ""); + } + + // A closing sequence of `#` characters is optional: + [Test] + public void LeafBlocksATXHeadings_Example041() + { + // Example 41 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ## foo ## + // ### bar ### + // + // Should be rendered as: + //

foo

+ //

bar

+ + Console.WriteLine("Example 41\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("## foo ##\n ### bar ###", "

foo

\n

bar

", ""); + } + + // It need not be the same length as the opening sequence: + [Test] + public void LeafBlocksATXHeadings_Example042() + { + // Example 42 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // # foo ################################## + // ##### foo ## + // + // Should be rendered as: + //

foo

+ //
foo
+ + Console.WriteLine("Example 42\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("# foo ##################################\n##### foo ##", "

foo

\n
foo
", ""); + } + + // Spaces are allowed after the closing sequence: + [Test] + public void LeafBlocksATXHeadings_Example043() + { + // Example 43 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ### foo ### + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 43\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("### foo ### ", "

foo

", ""); + } + + // A sequence of `#` characters with anything but [spaces] following it + // is not a closing sequence, but counts as part of the contents of the + // heading: + [Test] + public void LeafBlocksATXHeadings_Example044() + { + // Example 44 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ### foo ### b + // + // Should be rendered as: + //

foo ### b

+ + Console.WriteLine("Example 44\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("### foo ### b", "

foo ### b

", ""); + } + + // The closing sequence must be preceded by a space: + [Test] + public void LeafBlocksATXHeadings_Example045() + { + // Example 45 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // # foo# + // + // Should be rendered as: + //

foo#

+ + Console.WriteLine("Example 45\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("# foo#", "

foo#

", ""); + } + + // Backslash-escaped `#` characters do not count as part + // of the closing sequence: + [Test] + public void LeafBlocksATXHeadings_Example046() + { + // Example 46 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ### foo \### + // ## foo #\## + // # foo \# + // + // Should be rendered as: + //

foo ###

+ //

foo ###

+ //

foo #

+ + Console.WriteLine("Example 46\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("### foo \\###\n## foo #\\##\n# foo \\#", "

foo ###

\n

foo ###

\n

foo #

", ""); + } + + // ATX headings need not be separated from surrounding content by blank + // lines, and they can interrupt paragraphs: + [Test] + public void LeafBlocksATXHeadings_Example047() + { + // Example 47 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // **** + // ## foo + // **** + // + // Should be rendered as: + //
+ //

foo

+ //
+ + Console.WriteLine("Example 47\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("****\n## foo\n****", "
\n

foo

\n
", ""); + } + + [Test] + public void LeafBlocksATXHeadings_Example048() + { + // Example 48 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // Foo bar + // # baz + // Bar foo + // + // Should be rendered as: + //

Foo bar

+ //

baz

+ //

Bar foo

+ + Console.WriteLine("Example 48\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("Foo bar\n# baz\nBar foo", "

Foo bar

\n

baz

\n

Bar foo

", ""); + } + + // ATX headings can be empty: + [Test] + public void LeafBlocksATXHeadings_Example049() + { + // Example 49 + // Section: Leaf blocks / ATX headings + // + // The following Markdown: + // ## + // # + // ### ### + // + // Should be rendered as: + //

+ //

+ //

+ + Console.WriteLine("Example 49\nSection Leaf blocks / ATX headings\n"); + TestRoundtrip.TestSpec("## \n#\n### ###", "

\n

\n

", ""); + } + } + + [TestFixture] + public class TestLeafBlocksSetextHeadings + { + // ## Setext headings + // + // A [setext heading](@) consists of one or more + // lines of text, each containing at least one [non-whitespace + // character], with no more than 3 spaces indentation, followed by + // a [setext heading underline]. The lines of text must be such + // that, were they not followed by the setext heading underline, + // they would be interpreted as a paragraph: they cannot be + // interpretable as a [code fence], [ATX heading][ATX headings], + // [block quote][block quotes], [thematic break][thematic breaks], + // [list item][list items], or [HTML block][HTML blocks]. + // + // A [setext heading underline](@) is a sequence of + // `=` characters or a sequence of `-` characters, with no more than 3 + // spaces indentation and any number of trailing spaces. If a line + // containing a single `-` can be interpreted as an + // empty [list items], it should be interpreted this way + // and not as a [setext heading underline]. + // + // The heading is a level 1 heading if `=` characters are used in + // the [setext heading underline], and a level 2 heading if `-` + // characters are used. The contents of the heading are the result + // of parsing the preceding lines of text as CommonMark inline + // content. + // + // In general, a setext heading need not be preceded or followed by a + // blank line. However, it cannot interrupt a paragraph, so when a + // setext heading comes after a paragraph, a blank line is needed between + // them. + // + // Simple examples: + [Test] + public void LeafBlocksSetextHeadings_Example050() + { + // Example 50 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo *bar* + // ========= + // + // Foo *bar* + // --------- + // + // Should be rendered as: + //

Foo bar

+ //

Foo bar

+ + Console.WriteLine("Example 50\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo *bar*\n=========\n\nFoo *bar*\n---------", "

Foo bar

\n

Foo bar

", ""); + } + + // The content of the header may span more than one line: + [Test] + public void LeafBlocksSetextHeadings_Example051() + { + // Example 51 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo *bar + // baz* + // ==== + // + // Should be rendered as: + //

Foo bar + // baz

+ + Console.WriteLine("Example 51\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo *bar\nbaz*\n====", "

Foo bar\nbaz

", ""); + } + + // The contents are the result of parsing the headings's raw + // content as inlines. The heading's raw content is formed by + // concatenating the lines and removing initial and final + // [whitespace]. + [Test] + public void LeafBlocksSetextHeadings_Example052() + { + // Example 52 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo *bar + // baz*→ + // ==== + // + // Should be rendered as: + //

Foo bar + // baz

+ + Console.WriteLine("Example 52\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec(" Foo *bar\nbaz*\t\n====", "

Foo bar\nbaz

", ""); + } + + // The underlining can be any length: + [Test] + public void LeafBlocksSetextHeadings_Example053() + { + // Example 53 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // ------------------------- + // + // Foo + // = + // + // Should be rendered as: + //

Foo

+ //

Foo

+ + Console.WriteLine("Example 53\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\n-------------------------\n\nFoo\n=", "

Foo

\n

Foo

", ""); + } + + // The heading content can be indented up to three spaces, and need + // not line up with the underlining: + [Test] + public void LeafBlocksSetextHeadings_Example054() + { + // Example 54 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // --- + // + // Foo + // ----- + // + // Foo + // === + // + // Should be rendered as: + //

Foo

+ //

Foo

+ //

Foo

+ + Console.WriteLine("Example 54\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec(" Foo\n---\n\n Foo\n-----\n\n Foo\n ===", "

Foo

\n

Foo

\n

Foo

", ""); + } + + // Four spaces indent is too much: + [Test] + public void LeafBlocksSetextHeadings_Example055() + { + // Example 55 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // --- + // + // Foo + // --- + // + // Should be rendered as: + //
Foo
+            //     ---
+            //     
+            //     Foo
+            //     
+ //
+ + Console.WriteLine("Example 55\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec(" Foo\n ---\n\n Foo\n---", "
Foo\n---\n\nFoo\n
\n
", ""); + } + + // The setext heading underline can be indented up to three spaces, and + // may have trailing spaces: + [Test] + public void LeafBlocksSetextHeadings_Example056() + { + // Example 56 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // ---- + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 56\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\n ---- ", "

Foo

", ""); + } + + // Four spaces is too much: + [Test] + public void LeafBlocksSetextHeadings_Example057() + { + // Example 57 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // --- + // + // Should be rendered as: + //

Foo + // ---

+ + Console.WriteLine("Example 57\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\n ---", "

Foo\n---

", ""); + } + + // The setext heading underline cannot contain internal spaces: + [Test] + public void LeafBlocksSetextHeadings_Example058() + { + // Example 58 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // = = + // + // Foo + // --- - + // + // Should be rendered as: + //

Foo + // = =

+ //

Foo

+ //
+ + Console.WriteLine("Example 58\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\n= =\n\nFoo\n--- -", "

Foo\n= =

\n

Foo

\n
", ""); + } + + // Trailing spaces in the content line do not cause a line break: + [Test] + public void LeafBlocksSetextHeadings_Example059() + { + // Example 59 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // ----- + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 59\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo \n-----", "

Foo

", ""); + } + + // Nor does a backslash at the end: + [Test] + public void LeafBlocksSetextHeadings_Example060() + { + // Example 60 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo\ + // ---- + // + // Should be rendered as: + //

Foo\

+ + Console.WriteLine("Example 60\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\\\n----", "

Foo\\

", ""); + } + + // Since indicators of block structure take precedence over + // indicators of inline structure, the following are setext headings: + [Test] + public void LeafBlocksSetextHeadings_Example061() + { + // Example 61 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // `Foo + // ---- + // ` + // + // + // + // Should be rendered as: + //

`Foo

+ //

`

+ //

<a title="a lot

+ //

of dashes"/>

+ + Console.WriteLine("Example 61\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("`Foo\n----\n`\n\n
", "

`Foo

\n

`

\n

<a title="a lot

\n

of dashes"/>

", ""); + } + + // The setext heading underline cannot be a [lazy continuation + // line] in a list item or block quote: + [Test] + public void LeafBlocksSetextHeadings_Example062() + { + // Example 62 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // > Foo + // --- + // + // Should be rendered as: + //
+ //

Foo

+ //
+ //
+ + Console.WriteLine("Example 62\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("> Foo\n---", "
\n

Foo

\n
\n
", ""); + } + + [Test] + public void LeafBlocksSetextHeadings_Example063() + { + // Example 63 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // > foo + // bar + // === + // + // Should be rendered as: + //
+ //

foo + // bar + // ===

+ //
+ + Console.WriteLine("Example 63\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("> foo\nbar\n===", "
\n

foo\nbar\n===

\n
", ""); + } + + [Test] + public void LeafBlocksSetextHeadings_Example064() + { + // Example 64 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // - Foo + // --- + // + // Should be rendered as: + //
    + //
  • Foo
  • + //
+ //
+ + Console.WriteLine("Example 64\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("- Foo\n---", "
    \n
  • Foo
  • \n
\n
", ""); + } + + // A blank line is needed between a paragraph and a following + // setext heading, since otherwise the paragraph becomes part + // of the heading's content: + [Test] + public void LeafBlocksSetextHeadings_Example065() + { + // Example 65 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // Bar + // --- + // + // Should be rendered as: + //

Foo + // Bar

+ + Console.WriteLine("Example 65\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\nBar\n---", "

Foo\nBar

", ""); + } + + // But in general a blank line is not required before or after + // setext headings: + [Test] + public void LeafBlocksSetextHeadings_Example066() + { + // Example 66 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // --- + // Foo + // --- + // Bar + // --- + // Baz + // + // Should be rendered as: + //
+ //

Foo

+ //

Bar

+ //

Baz

+ + Console.WriteLine("Example 66\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("---\nFoo\n---\nBar\n---\nBaz", "
\n

Foo

\n

Bar

\n

Baz

", ""); + } + + // Setext headings cannot be empty: + [Test] + public void LeafBlocksSetextHeadings_Example067() + { + // Example 67 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // + // ==== + // + // Should be rendered as: + //

====

+ + Console.WriteLine("Example 67\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("\n====", "

====

", ""); + } + + // Setext heading text lines must not be interpretable as block + // constructs other than paragraphs. So, the line of dashes + // in these examples gets interpreted as a thematic break: + [Test] + public void LeafBlocksSetextHeadings_Example068() + { + // Example 68 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // --- + // --- + // + // Should be rendered as: + //
+ //
+ + Console.WriteLine("Example 68\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("---\n---", "
\n
", ""); + } + + [Test] + public void LeafBlocksSetextHeadings_Example069() + { + // Example 69 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // - foo + // ----- + // + // Should be rendered as: + //
    + //
  • foo
  • + //
+ //
+ + Console.WriteLine("Example 69\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("- foo\n-----", "
    \n
  • foo
  • \n
\n
", ""); + } + + [Test] + public void LeafBlocksSetextHeadings_Example070() + { + // Example 70 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // foo + // --- + // + // Should be rendered as: + //
foo
+            //     
+ //
+ + Console.WriteLine("Example 70\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec(" foo\n---", "
foo\n
\n
", ""); + } + + [Test] + public void LeafBlocksSetextHeadings_Example071() + { + // Example 71 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // > foo + // ----- + // + // Should be rendered as: + //
+ //

foo

+ //
+ //
+ + Console.WriteLine("Example 71\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("> foo\n-----", "
\n

foo

\n
\n
", ""); + } + + // If you want a heading with `> foo` as its literal text, you can + // use backslash escapes: + [Test] + public void LeafBlocksSetextHeadings_Example072() + { + // Example 72 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // \> foo + // ------ + // + // Should be rendered as: + //

> foo

+ + Console.WriteLine("Example 72\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("\\> foo\n------", "

> foo

", ""); + } + + // **Compatibility note:** Most existing Markdown implementations + // do not allow the text of setext headings to span multiple lines. + // But there is no consensus about how to interpret + // + // ``` markdown + // Foo + // bar + // --- + // baz + // ``` + // + // One can find four different interpretations: + // + // 1. paragraph "Foo", heading "bar", paragraph "baz" + // 2. paragraph "Foo bar", thematic break, paragraph "baz" + // 3. paragraph "Foo bar --- baz" + // 4. heading "Foo bar", paragraph "baz" + // + // We find interpretation 4 most natural, and interpretation 4 + // increases the expressive power of CommonMark, by allowing + // multiline headings. Authors who want interpretation 1 can + // put a blank line after the first paragraph: + [Test] + public void LeafBlocksSetextHeadings_Example073() + { + // Example 73 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // + // bar + // --- + // baz + // + // Should be rendered as: + //

Foo

+ //

bar

+ //

baz

+ + Console.WriteLine("Example 73\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\n\nbar\n---\nbaz", "

Foo

\n

bar

\n

baz

", ""); + } + + // Authors who want interpretation 2 can put blank lines around + // the thematic break, + [Test] + public void LeafBlocksSetextHeadings_Example074() + { + // Example 74 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // bar + // + // --- + // + // baz + // + // Should be rendered as: + //

Foo + // bar

+ //
+ //

baz

+ + Console.WriteLine("Example 74\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\nbar\n\n---\n\nbaz", "

Foo\nbar

\n
\n

baz

", ""); + } + + // or use a thematic break that cannot count as a [setext heading + // underline], such as + [Test] + public void LeafBlocksSetextHeadings_Example075() + { + // Example 75 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // bar + // * * * + // baz + // + // Should be rendered as: + //

Foo + // bar

+ //
+ //

baz

+ + Console.WriteLine("Example 75\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\nbar\n* * *\nbaz", "

Foo\nbar

\n
\n

baz

", ""); + } + + // Authors who want interpretation 3 can use backslash escapes: + [Test] + public void LeafBlocksSetextHeadings_Example076() + { + // Example 76 + // Section: Leaf blocks / Setext headings + // + // The following Markdown: + // Foo + // bar + // \--- + // baz + // + // Should be rendered as: + //

Foo + // bar + // --- + // baz

+ + Console.WriteLine("Example 76\nSection Leaf blocks / Setext headings\n"); + TestRoundtrip.TestSpec("Foo\nbar\n\\---\nbaz", "

Foo\nbar\n---\nbaz

", ""); + } + } + + [TestFixture] + public class TestLeafBlocksIndentedCodeBlocks + { + // ## Indented code blocks + // + // An [indented code block](@) is composed of one or more + // [indented chunks] separated by blank lines. + // An [indented chunk](@) is a sequence of non-blank lines, + // each indented four or more spaces. The contents of the code block are + // the literal contents of the lines, including trailing + // [line endings], minus four spaces of indentation. + // An indented code block has no [info string]. + // + // An indented code block cannot interrupt a paragraph, so there must be + // a blank line between a paragraph and a following indented code block. + // (A blank line is not needed, however, between a code block and a following + // paragraph.) + [Test] + public void LeafBlocksIndentedCodeBlocks_Example077() + { + // Example 77 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // a simple + // indented code block + // + // Should be rendered as: + //
a simple
+            //       indented code block
+            //     
+ + Console.WriteLine("Example 77\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" a simple\n indented code block", "
a simple\n  indented code block\n
", ""); + } + + // If there is any ambiguity between an interpretation of indentation + // as a code block and as indicating that material belongs to a [list + // item][list items], the list item interpretation takes precedence: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example078() + { + // Example 78 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // - foo + // + // bar + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //

    bar

    + //
  • + //
+ + Console.WriteLine("Example 78\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" - foo\n\n bar", "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
", ""); + } + + [Test] + public void LeafBlocksIndentedCodeBlocks_Example079() + { + // Example 79 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // 1. foo + // + // - bar + // + // Should be rendered as: + //
    + //
  1. + //

    foo

    + //
      + //
    • bar
    • + //
    + //
  2. + //
+ + Console.WriteLine("Example 79\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec("1. foo\n\n - bar", "
    \n
  1. \n

    foo

    \n
      \n
    • bar
    • \n
    \n
  2. \n
", ""); + } + + // The contents of a code block are literal text, and do not get parsed + // as Markdown: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example080() + { + // Example 80 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + //
+ // *hi* + // + // - one + // + // Should be rendered as: + //
<a/>
+            //     *hi*
+            //     
+            //     - one
+            //     
+ + Console.WriteLine("Example 80\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec("
\n *hi*\n\n - one", "
<a/>\n*hi*\n\n- one\n
", ""); + } + + // Here we have three chunks separated by blank lines: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example081() + { + // Example 81 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // chunk1 + // + // chunk2 + // + // + // + // chunk3 + // + // Should be rendered as: + //
chunk1
+            //     
+            //     chunk2
+            //     
+            //     
+            //     
+            //     chunk3
+            //     
+ + Console.WriteLine("Example 81\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" chunk1\n\n chunk2\n \n \n \n chunk3", "
chunk1\n\nchunk2\n\n\n\nchunk3\n
", ""); + } + + // Any initial spaces beyond four will be included in the content, even + // in interior blank lines: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example082() + { + // Example 82 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // chunk1 + // + // chunk2 + // + // Should be rendered as: + //
chunk1
+            //       
+            //       chunk2
+            //     
+ + Console.WriteLine("Example 82\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" chunk1\n \n chunk2", "
chunk1\n  \n  chunk2\n
", ""); + } + + // An indented code block cannot interrupt a paragraph. (This + // allows hanging indents and the like.) + [Test] + public void LeafBlocksIndentedCodeBlocks_Example083() + { + // Example 83 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // Foo + // bar + // + // + // Should be rendered as: + //

Foo + // bar

+ + Console.WriteLine("Example 83\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec("Foo\n bar\n", "

Foo\nbar

", ""); + } + + // However, any non-blank line with fewer than four leading spaces ends + // the code block immediately. So a paragraph may occur immediately + // after indented code: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example084() + { + // Example 84 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // foo + // bar + // + // Should be rendered as: + //
foo
+            //     
+ //

bar

+ + Console.WriteLine("Example 84\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" foo\nbar", "
foo\n
\n

bar

", ""); + } + + // And indented code can occur immediately before and after other kinds of + // blocks: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example085() + { + // Example 85 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // # Heading + // foo + // Heading + // ------ + // foo + // ---- + // + // Should be rendered as: + //

Heading

+ //
foo
+            //     
+ //

Heading

+ //
foo
+            //     
+ //
+ + Console.WriteLine("Example 85\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec("# Heading\n foo\nHeading\n------\n foo\n----", "

Heading

\n
foo\n
\n

Heading

\n
foo\n
\n
", ""); + } + + // The first line can be indented more than four spaces: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example086() + { + // Example 86 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // foo + // bar + // + // Should be rendered as: + //
    foo
+            //     bar
+            //     
+ + Console.WriteLine("Example 86\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" foo\n bar", "
    foo\nbar\n
", ""); + } + + // Blank lines preceding or following an indented code block + // are not included in it: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example087() + { + // Example 87 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // + // + // foo + // + // + // + // Should be rendered as: + //
foo
+            //     
+ + Console.WriteLine("Example 87\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec("\n \n foo\n \n", "
foo\n
", ""); + } + + // Trailing spaces are included in the code block's content: + [Test] + public void LeafBlocksIndentedCodeBlocks_Example088() + { + // Example 88 + // Section: Leaf blocks / Indented code blocks + // + // The following Markdown: + // foo + // + // Should be rendered as: + //
foo  
+            //     
+ + Console.WriteLine("Example 88\nSection Leaf blocks / Indented code blocks\n"); + TestRoundtrip.TestSpec(" foo ", "
foo  \n
", ""); + } + } + + [TestFixture] + public class TestLeafBlocksFencedCodeBlocks + { + // ## Fenced code blocks + // + // A [code fence](@) is a sequence + // of at least three consecutive backtick characters (`` ` ``) or + // tildes (`~`). (Tildes and backticks cannot be mixed.) + // A [fenced code block](@) + // begins with a code fence, indented no more than three spaces. + // + // The line with the opening code fence may optionally contain some text + // following the code fence; this is trimmed of leading and trailing + // whitespace and called the [info string](@). If the [info string] comes + // after a backtick fence, it may not contain any backtick + // characters. (The reason for this restriction is that otherwise + // some inline code would be incorrectly interpreted as the + // beginning of a fenced code block.) + // + // The content of the code block consists of all subsequent lines, until + // a closing [code fence] of the same type as the code block + // began with (backticks or tildes), and with at least as many backticks + // or tildes as the opening code fence. If the leading code fence is + // indented N spaces, then up to N spaces of indentation are removed from + // each line of the content (if present). (If a content line is not + // indented, it is preserved unchanged. If it is indented less than N + // spaces, all of the indentation is removed.) + // + // The closing code fence may be indented up to three spaces, and may be + // followed only by spaces, which are ignored. If the end of the + // containing block (or document) is reached and no closing code fence + // has been found, the code block contains all of the lines after the + // opening code fence until the end of the containing block (or + // document). (An alternative spec would require backtracking in the + // event that a closing code fence is not found. But this makes parsing + // much less efficient, and there seems to be no real down side to the + // behavior described here.) + // + // A fenced code block may interrupt a paragraph, and does not require + // a blank line either before or after. + // + // The content of a code fence is treated as literal text, not parsed + // as inlines. The first word of the [info string] is typically used to + // specify the language of the code sample, and rendered in the `class` + // attribute of the `code` tag. However, this spec does not mandate any + // particular treatment of the [info string]. + // + // Here is a simple example with backticks: + [Test] + public void LeafBlocksFencedCodeBlocks_Example089() + { + // Example 89 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // < + // > + // ``` + // + // Should be rendered as: + //
<
+            //      >
+            //     
+ + Console.WriteLine("Example 89\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\n<\n >\n```", "
<\n >\n
", ""); + } + + // With tildes: + [Test] + public void LeafBlocksFencedCodeBlocks_Example090() + { + // Example 90 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ~~~ + // < + // > + // ~~~ + // + // Should be rendered as: + //
<
+            //      >
+            //     
+ + Console.WriteLine("Example 90\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("~~~\n<\n >\n~~~", "
<\n >\n
", ""); + } + + // Fewer than three backticks is not enough: + [Test] + public void LeafBlocksFencedCodeBlocks_Example091() + { + // Example 91 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // `` + // foo + // `` + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 91\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("``\nfoo\n``", "

foo

", ""); + } + + // The closing code fence must use the same character as the opening + // fence: + [Test] + public void LeafBlocksFencedCodeBlocks_Example092() + { + // Example 92 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // ~~~ + // ``` + // + // Should be rendered as: + //
aaa
+            //     ~~~
+            //     
+ + Console.WriteLine("Example 92\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\naaa\n~~~\n```", "
aaa\n~~~\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example093() + { + // Example 93 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ~~~ + // aaa + // ``` + // ~~~ + // + // Should be rendered as: + //
aaa
+            //     ```
+            //     
+ + Console.WriteLine("Example 93\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("~~~\naaa\n```\n~~~", "
aaa\n```\n
", ""); + } + + // The closing code fence must be at least as long as the opening fence: + [Test] + public void LeafBlocksFencedCodeBlocks_Example094() + { + // Example 94 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ```` + // aaa + // ``` + // `````` + // + // Should be rendered as: + //
aaa
+            //     ```
+            //     
+ + Console.WriteLine("Example 94\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("````\naaa\n```\n``````", "
aaa\n```\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example095() + { + // Example 95 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ~~~~ + // aaa + // ~~~ + // ~~~~ + // + // Should be rendered as: + //
aaa
+            //     ~~~
+            //     
+ + Console.WriteLine("Example 95\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("~~~~\naaa\n~~~\n~~~~", "
aaa\n~~~\n
", ""); + } + + // Unclosed code blocks are closed by the end of the document + // (or the enclosing [block quote][block quotes] or [list item][list items]): + [Test] + public void LeafBlocksFencedCodeBlocks_Example096() + { + // Example 96 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 96\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```", "
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example097() + { + // Example 97 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ````` + // + // ``` + // aaa + // + // Should be rendered as: + //

+            //     ```
+            //     aaa
+            //     
+ + Console.WriteLine("Example 97\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("`````\n\n```\naaa", "
\n```\naaa\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example098() + { + // Example 98 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // > ``` + // > aaa + // + // bbb + // + // Should be rendered as: + //
+ //
aaa
+            //     
+ //
+ //

bbb

+ + Console.WriteLine("Example 98\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("> ```\n> aaa\n\nbbb", "
\n
aaa\n
\n
\n

bbb

", ""); + } + + // A code block can have all empty lines as its content: + [Test] + public void LeafBlocksFencedCodeBlocks_Example099() + { + // Example 99 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // + // + // ``` + // + // Should be rendered as: + //

+            //       
+            //     
+ + Console.WriteLine("Example 99\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\n\n \n```", "
\n  \n
", ""); + } + + // A code block can be empty: + [Test] + public void LeafBlocksFencedCodeBlocks_Example100() + { + // Example 100 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // ``` + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 100\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\n```", "
", ""); + } + + // Fences can be indented. If the opening fence is indented, + // content lines will have equivalent opening indentation removed, + // if present: + [Test] + public void LeafBlocksFencedCodeBlocks_Example101() + { + // Example 101 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // aaa + // ``` + // + // Should be rendered as: + //
aaa
+            //     aaa
+            //     
+ + Console.WriteLine("Example 101\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec(" ```\n aaa\naaa\n```", "
aaa\naaa\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example102() + { + // Example 102 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // aaa + // aaa + // ``` + // + // Should be rendered as: + //
aaa
+            //     aaa
+            //     aaa
+            //     
+ + Console.WriteLine("Example 102\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec(" ```\naaa\n aaa\naaa\n ```", "
aaa\naaa\naaa\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example103() + { + // Example 103 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // aaa + // aaa + // ``` + // + // Should be rendered as: + //
aaa
+            //      aaa
+            //     aaa
+            //     
+ + Console.WriteLine("Example 103\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec(" ```\n aaa\n aaa\n aaa\n ```", "
aaa\n aaa\naaa\n
", ""); + } + + // Four spaces indentation produces an indented code block: + [Test] + public void LeafBlocksFencedCodeBlocks_Example104() + { + // Example 104 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // ``` + // + // Should be rendered as: + //
```
+            //     aaa
+            //     ```
+            //     
+ + Console.WriteLine("Example 104\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec(" ```\n aaa\n ```", "
```\naaa\n```\n
", ""); + } + + // Closing fences may be indented by 0-3 spaces, and their indentation + // need not match that of the opening fence: + [Test] + public void LeafBlocksFencedCodeBlocks_Example105() + { + // Example 105 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // ``` + // + // Should be rendered as: + //
aaa
+            //     
+ + Console.WriteLine("Example 105\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\naaa\n ```", "
aaa\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example106() + { + // Example 106 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // ``` + // + // Should be rendered as: + //
aaa
+            //     
+ + Console.WriteLine("Example 106\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec(" ```\naaa\n ```", "
aaa\n
", ""); + } + + // This is not a closing fence, because it is indented 4 spaces: + [Test] + public void LeafBlocksFencedCodeBlocks_Example107() + { + // Example 107 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // aaa + // ``` + // + // Should be rendered as: + //
aaa
+            //         ```
+            //     
+ + Console.WriteLine("Example 107\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\naaa\n ```", "
aaa\n    ```\n
", ""); + } + + // Code fences (opening and closing) cannot contain internal spaces: + [Test] + public void LeafBlocksFencedCodeBlocks_Example108() + { + // Example 108 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` ``` + // aaa + // + // Should be rendered as: + //

+ // aaa

+ + Console.WriteLine("Example 108\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("``` ```\naaa", "

\naaa

", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example109() + { + // Example 109 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ~~~~~~ + // aaa + // ~~~ ~~ + // + // Should be rendered as: + //
aaa
+            //     ~~~ ~~
+            //     
+ + Console.WriteLine("Example 109\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("~~~~~~\naaa\n~~~ ~~", "
aaa\n~~~ ~~\n
", ""); + } + + // Fenced code blocks can interrupt paragraphs, and can be followed + // directly by paragraphs, without a blank line between: + [Test] + public void LeafBlocksFencedCodeBlocks_Example110() + { + // Example 110 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // foo + // ``` + // bar + // ``` + // baz + // + // Should be rendered as: + //

foo

+ //
bar
+            //     
+ //

baz

+ + Console.WriteLine("Example 110\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("foo\n```\nbar\n```\nbaz", "

foo

\n
bar\n
\n

baz

", ""); + } + + // Other blocks can also occur before and after fenced code blocks + // without an intervening blank line: + [Test] + public void LeafBlocksFencedCodeBlocks_Example111() + { + // Example 111 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // foo + // --- + // ~~~ + // bar + // ~~~ + // # baz + // + // Should be rendered as: + //

foo

+ //
bar
+            //     
+ //

baz

+ + Console.WriteLine("Example 111\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("foo\n---\n~~~\nbar\n~~~\n# baz", "

foo

\n
bar\n
\n

baz

", ""); + } + + // An [info string] can be provided after the opening code fence. + // Although this spec doesn't mandate any particular treatment of + // the info string, the first word is typically used to specify + // the language of the code block. In HTML output, the language is + // normally indicated by adding a class to the `code` element consisting + // of `language-` followed by the language name. + [Test] + public void LeafBlocksFencedCodeBlocks_Example112() + { + // Example 112 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ```ruby + // def foo(x) + // return 3 + // end + // ``` + // + // Should be rendered as: + //
def foo(x)
+            //       return 3
+            //     end
+            //     
+ + Console.WriteLine("Example 112\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```ruby\ndef foo(x)\n return 3\nend\n```", "
def foo(x)\n  return 3\nend\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example113() + { + // Example 113 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ~~~~ ruby startline=3 $%@#$ + // def foo(x) + // return 3 + // end + // ~~~~~~~ + // + // Should be rendered as: + //
def foo(x)
+            //       return 3
+            //     end
+            //     
+ + Console.WriteLine("Example 113\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~", "
def foo(x)\n  return 3\nend\n
", ""); + } + + [Test] + public void LeafBlocksFencedCodeBlocks_Example114() + { + // Example 114 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ````; + // ```` + // + // Should be rendered as: + //
+ + Console.WriteLine("Example 114\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("````;\n````", "
", ""); + } + + // [Info strings] for backtick code blocks cannot contain backticks: + [Test] + public void LeafBlocksFencedCodeBlocks_Example115() + { + // Example 115 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` aa ``` + // foo + // + // Should be rendered as: + //

aa + // foo

+ + Console.WriteLine("Example 115\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("``` aa ```\nfoo", "

aa\nfoo

", ""); + } + + // [Info strings] for tilde code blocks can contain backticks and tildes: + [Test] + public void LeafBlocksFencedCodeBlocks_Example116() + { + // Example 116 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ~~~ aa ``` ~~~ + // foo + // ~~~ + // + // Should be rendered as: + //
foo
+            //     
+ + Console.WriteLine("Example 116\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("~~~ aa ``` ~~~\nfoo\n~~~", "
foo\n
", ""); + } + + // Closing code fences cannot have [info strings]: + [Test] + public void LeafBlocksFencedCodeBlocks_Example117() + { + // Example 117 + // Section: Leaf blocks / Fenced code blocks + // + // The following Markdown: + // ``` + // ``` aaa + // ``` + // + // Should be rendered as: + //
``` aaa
+            //     
+ + Console.WriteLine("Example 117\nSection Leaf blocks / Fenced code blocks\n"); + TestRoundtrip.TestSpec("```\n``` aaa\n```", "
``` aaa\n
", ""); + } + } + + [TestFixture] + public class TestLeafBlocksHTMLBlocks + { + // ## HTML blocks + // + // An [HTML block](@) is a group of lines that is treated + // as raw HTML (and will not be escaped in HTML output). + // + // There are seven kinds of [HTML block], which can be defined by their + // start and end conditions. The block begins with a line that meets a + // [start condition](@) (after up to three spaces optional indentation). + // It ends with the first subsequent line that meets a matching [end + // condition](@), or the last line of the document, or the last line of + // the [container block](#container-blocks) containing the current HTML + // block, if no line is encountered that meets the [end condition]. If + // the first line meets both the [start condition] and the [end + // condition], the block will contain just that line. + // + // 1. **Start condition:** line begins with the string ``, or the end of the line.\ + // **End condition:** line contains an end tag + // ``, ``, or `` (case-insensitive; it + // need not match the start tag). + // + // 2. **Start condition:** line begins with the string ``. + // + // 3. **Start condition:** line begins with the string ``. + // + // 4. **Start condition:** line begins with the string ``. + // + // 5. **Start condition:** line begins with the string + // ``. + // + // 6. **Start condition:** line begins the string `<` or ``, or + // the string `/>`.\ + // **End condition:** line is followed by a [blank line]. + // + // 7. **Start condition:** line begins with a complete [open tag] + // (with any [tag name] other than `script`, + // `style`, or `pre`) or a complete [closing tag], + // followed only by [whitespace] or the end of the line.\ + // **End condition:** line is followed by a [blank line]. + // + // HTML blocks continue until they are closed by their appropriate + // [end condition], or the last line of the document or other [container + // block](#container-blocks). This means any HTML **within an HTML + // block** that might otherwise be recognised as a start condition will + // be ignored by the parser and passed through as-is, without changing + // the parser's state. + // + // For instance, `
` within a HTML block started by `` will not affect
+        // the parser state; as the HTML block was started in by start condition 6, it
+        // will end at any blank line. This can be surprising:
+        [Test]
+        public void LeafBlocksHTMLBlocks_Example118()
+        {
+            // Example 118
+            // Section: Leaf blocks / HTML blocks
+            //
+            // The following Markdown:
+            //     
+ //
+            //     **Hello**,
+            //     
+            //     _world_.
+            //     
+ //
+ // + // Should be rendered as: + //
+ //
+            //     **Hello**,
+            //     

world. + //

+ //
+ + Console.WriteLine("Example 118\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n
\n**Hello**,\n\n_world_.\n
\n
", "
\n
\n**Hello**,\n

world.\n

\n
", ""); + } + + // In this case, the HTML block is terminated by the newline — the `**Hello**` + // text remains verbatim — and regular parsing resumes, with a paragraph, + // emphasised `world` and inline and block HTML following. + // + // All types of [HTML blocks] except type 7 may interrupt + // a paragraph. Blocks of type 7 may not interrupt a paragraph. + // (This restriction is intended to prevent unwanted interpretation + // of long tags inside a wrapped paragraph as starting HTML blocks.) + // + // Some simple examples follow. Here are some basic HTML blocks + // of type 6: + [Test] + public void LeafBlocksHTMLBlocks_Example119() + { + // Example 119 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // + // + // + //
+ // hi + //
+ // + // okay. + // + // Should be rendered as: + // + // + // + // + //
+ // hi + //
+ //

okay.

+ + Console.WriteLine("Example 119\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n \n \n \n
\n hi\n
\n\nokay.", "\n \n \n \n
\n hi\n
\n

okay.

", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example120() + { + // Example 120 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
\n*foo*", ""); + } + + // Here we have two HTML blocks with a Markdown paragraph between them: + [Test] + public void LeafBlocksHTMLBlocks_Example122() + { + // Example 122 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // + // *Markdown* + // + //
+ // + // Should be rendered as: + //
+ //

Markdown

+ //
+ + Console.WriteLine("Example 122\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n\n*Markdown*\n\n
", "
\n

Markdown

\n
", ""); + } + + // The tag on the first line can be partial, as long + // as it is split where there would be whitespace: + [Test] + public void LeafBlocksHTMLBlocks_Example123() + { + // Example 123 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ //
+ // + // Should be rendered as: + //
+ //
+ + Console.WriteLine("Example 123\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n
", "
\n
", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example124() + { + // Example 124 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ //
+ // + // Should be rendered as: + //
+ //
+ + Console.WriteLine("Example 124\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n
", "
\n
", ""); + } + + // An open tag need not be closed: + [Test] + public void LeafBlocksHTMLBlocks_Example125() + { + // Example 125 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // *foo* + // + // *bar* + // + // Should be rendered as: + //
+ // *foo* + //

bar

+ + Console.WriteLine("Example 125\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n*foo*\n\n*bar*", "
\n*foo*\n

bar

", ""); + } + + // A partial tag need not even be completed (garbage + // in, garbage out): + [Test] + public void LeafBlocksHTMLBlocks_Example126() + { + // Example 126 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // + // Should be rendered as: + // + + Console.WriteLine("Example 129\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("", "", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example130() + { + // Example 130 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // foo + //
+ // + // Should be rendered as: + //
+ // foo + //
+ + Console.WriteLine("Example 130\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\nfoo\n
", "
\nfoo\n
", ""); + } + + // Everything until the next blank line or end of document + // gets included in the HTML block. So, in the following + // example, what looks like a Markdown code block + // is actually part of the HTML block, which continues until a blank + // line or the end of the document is reached: + [Test] + public void LeafBlocksHTMLBlocks_Example131() + { + // Example 131 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // ``` c + // int x = 33; + // ``` + // + // Should be rendered as: + //
+ // ``` c + // int x = 33; + // ``` + + Console.WriteLine("Example 131\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n``` c\nint x = 33;\n```", "
\n``` c\nint x = 33;\n```", ""); + } + + // To start an [HTML block] with a tag that is *not* in the + // list of block-level tags in (6), you must put the tag by + // itself on the first line (and it must be complete): + [Test] + public void LeafBlocksHTMLBlocks_Example132() + { + // Example 132 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // *bar* + // + // + // Should be rendered as: + // + // *bar* + // + + Console.WriteLine("Example 132\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n*bar*\n", "\n*bar*\n", ""); + } + + // In type 7 blocks, the [tag name] can be anything: + [Test] + public void LeafBlocksHTMLBlocks_Example133() + { + // Example 133 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // *bar* + // + // + // Should be rendered as: + // + // *bar* + // + + Console.WriteLine("Example 133\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n*bar*\n", "\n*bar*\n", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example134() + { + // Example 134 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // *bar* + // + // + // Should be rendered as: + // + // *bar* + // + + Console.WriteLine("Example 134\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n*bar*\n", "\n*bar*\n", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example135() + { + // Example 135 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // *bar* + // + // Should be rendered as: + // + // *bar* + + Console.WriteLine("Example 135\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n*bar*", "\n*bar*", ""); + } + + // These rules are designed to allow us to work with tags that + // can function as either block-level or inline-level tags. + // The `` tag is a nice example. We can surround content with + // `` tags in three different ways. In this case, we get a raw + // HTML block, because the `` tag is on a line by itself: + [Test] + public void LeafBlocksHTMLBlocks_Example136() + { + // Example 136 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // *foo* + // + // + // Should be rendered as: + // + // *foo* + // + + Console.WriteLine("Example 136\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n*foo*\n", "\n*foo*\n", ""); + } + + // In this case, we get a raw HTML block that just includes + // the `` tag (because it ends with the following blank + // line). So the contents get interpreted as CommonMark: + [Test] + public void LeafBlocksHTMLBlocks_Example137() + { + // Example 137 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // + // *foo* + // + // + // + // Should be rendered as: + // + //

foo

+ //
+ + Console.WriteLine("Example 137\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n\n*foo*\n\n", "\n

foo

\n
", ""); + } + + // Finally, in this case, the `` tags are interpreted + // as [raw HTML] *inside* the CommonMark paragraph. (Because + // the tag is not on a line by itself, we get inline HTML + // rather than an [HTML block].) + [Test] + public void LeafBlocksHTMLBlocks_Example138() + { + // Example 138 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // *foo* + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 138\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("*foo*", "

foo

", ""); + } + + // HTML tags designed to contain literal content + // (`script`, `style`, `pre`), comments, processing instructions, + // and declarations are treated somewhat differently. + // Instead of ending at the first blank line, these blocks + // end at the first line containing a corresponding end tag. + // As a result, these blocks can contain blank lines: + // + // A pre tag (type 1): + [Test] + public void LeafBlocksHTMLBlocks_Example139() + { + // Example 139 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //

+            //     import Text.HTML.TagSoup
+            //     
+            //     main :: IO ()
+            //     main = print $ parseTags tags
+            //     
+ // okay + // + // Should be rendered as: + //

+            //     import Text.HTML.TagSoup
+            //     
+            //     main :: IO ()
+            //     main = print $ parseTags tags
+            //     
+ //

okay

+ + Console.WriteLine("Example 139\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\nokay", "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\n

okay

", ""); + } + + // A script tag (type 1): + [Test] + public void LeafBlocksHTMLBlocks_Example140() + { + // Example 140 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // okay + // + // Should be rendered as: + // + //

okay

+ + Console.WriteLine("Example 140\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\nokay", "\n

okay

", ""); + } + + // A style tag (type 1): + [Test] + public void LeafBlocksHTMLBlocks_Example141() + { + // Example 141 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // okay + // + // Should be rendered as: + // + //

okay

+ + Console.WriteLine("Example 141\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\nh1 {color:red;}\n\np {color:blue;}\n\nokay", "\nh1 {color:red;}\n\np {color:blue;}\n\n

okay

", ""); + } + + // If there is no matching end tag, the block will end at the + // end of the document (or the enclosing [block quote][block quotes] + // or [list item][list items]): + [Test] + public void LeafBlocksHTMLBlocks_Example142() + { + // Example 142 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // *foo* + // + // Should be rendered as: + // + //

foo

+ + Console.WriteLine("Example 145\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n*foo*", "\n

foo

", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example146() + { + // Example 146 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // *bar* + // *baz* + // + // Should be rendered as: + // *bar* + //

baz

+ + Console.WriteLine("Example 146\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("*bar*\n*baz*", "*bar*\n

baz

", ""); + } + + // Note that anything on the last line after the + // end tag will be included in the [HTML block]: + [Test] + public void LeafBlocksHTMLBlocks_Example147() + { + // Example 147 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // 1. *bar* + // + // Should be rendered as: + // 1. *bar* + + Console.WriteLine("Example 147\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("1. *bar*", "1. *bar*", ""); + } + + // A comment (type 2): + [Test] + public void LeafBlocksHTMLBlocks_Example148() + { + // Example 148 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // okay + // + // Should be rendered as: + // + //

okay

+ + Console.WriteLine("Example 148\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\nokay", "\n

okay

", ""); + } + + // A processing instruction (type 3): + [Test] + public void LeafBlocksHTMLBlocks_Example149() + { + // Example 149 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // '; + // + // ?> + // okay + // + // Should be rendered as: + // '; + // + // ?> + //

okay

+ + Console.WriteLine("Example 149\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("';\n\n?>\nokay", "';\n\n?>\n

okay

", ""); + } + + // A declaration (type 4): + [Test] + public void LeafBlocksHTMLBlocks_Example150() + { + // Example 150 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // + // Should be rendered as: + // + + Console.WriteLine("Example 150\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("", "", ""); + } + + // CDATA (type 5): + [Test] + public void LeafBlocksHTMLBlocks_Example151() + { + // Example 151 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // okay + // + // Should be rendered as: + // + //

okay

+ + Console.WriteLine("Example 151\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\nokay", "\n

okay

", ""); + } + + // The opening tag can be indented 1-3 spaces, but not 4: + [Test] + public void LeafBlocksHTMLBlocks_Example152() + { + // Example 152 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // + // + // + // Should be rendered as: + // + //
<!-- foo -->
+            //     
+ + Console.WriteLine("Example 152\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec(" \n\n ", " \n
<!-- foo -->\n
", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example153() + { + // Example 153 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // + //
+ // + // Should be rendered as: + //
+ //
<div>
+            //     
+ + Console.WriteLine("Example 153\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n\n
", "
\n
<div>\n
", ""); + } + + // An HTML block of types 1--6 can interrupt a paragraph, and need not be + // preceded by a blank line. + [Test] + public void LeafBlocksHTMLBlocks_Example154() + { + // Example 154 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // Foo + //
+ // bar + //
+ // + // Should be rendered as: + //

Foo

+ //
+ // bar + //
+ + Console.WriteLine("Example 154\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("Foo\n
\nbar\n
", "

Foo

\n
\nbar\n
", ""); + } + + // However, a following blank line is needed, except at the end of + // a document, and except for blocks of types 1--5, [above][HTML + // block]: + [Test] + public void LeafBlocksHTMLBlocks_Example155() + { + // Example 155 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // bar + //
+ // *foo* + // + // Should be rendered as: + //
+ // bar + //
+ // *foo* + + Console.WriteLine("Example 155\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\nbar\n
\n*foo*", "
\nbar\n
\n*foo*", ""); + } + + // HTML blocks of type 7 cannot interrupt a paragraph: + [Test] + public void LeafBlocksHTMLBlocks_Example156() + { + // Example 156 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // Foo + // + // baz + // + // Should be rendered as: + //

Foo + // + // baz

+ + Console.WriteLine("Example 156\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("Foo\n\nbaz", "

Foo\n\nbaz

", ""); + } + + // This rule differs from John Gruber's original Markdown syntax + // specification, which says: + // + // > The only restrictions are that block-level HTML elements — + // > e.g. `
`, ``, `
`, `

`, etc. — must be separated from + // > surrounding content by blank lines, and the start and end tags of the + // > block should not be indented with tabs or spaces. + // + // In some ways Gruber's rule is more restrictive than the one given + // here: + // + // - It requires that an HTML block be preceded by a blank line. + // - It does not allow the start tag to be indented. + // - It requires a matching end tag, which it also does not allow to + // be indented. + // + // Most Markdown implementations (including some of Gruber's own) do not + // respect all of these restrictions. + // + // There is one respect, however, in which Gruber's rule is more liberal + // than the one given here, since it allows blank lines to occur inside + // an HTML block. There are two reasons for disallowing them here. + // First, it removes the need to parse balanced tags, which is + // expensive and can require backtracking from the end of the document + // if no matching end tag is found. Second, it provides a very simple + // and flexible way of including Markdown content inside HTML tags: + // simply separate the Markdown from the HTML using blank lines: + // + // Compare: + [Test] + public void LeafBlocksHTMLBlocks_Example157() + { + // Example 157 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //

+ // + // *Emphasized* text. + // + //
+ // + // Should be rendered as: + //
+ //

Emphasized text.

+ //
+ + Console.WriteLine("Example 157\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n\n*Emphasized* text.\n\n
", "
\n

Emphasized text.

\n
", ""); + } + + [Test] + public void LeafBlocksHTMLBlocks_Example158() + { + // Example 158 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // *Emphasized* text. + //
+ // + // Should be rendered as: + //
+ // *Emphasized* text. + //
+ + Console.WriteLine("Example 158\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("
\n*Emphasized* text.\n
", "
\n*Emphasized* text.\n
", ""); + } + + // Some Markdown implementations have adopted a convention of + // interpreting content inside tags as text if the open tag has + // the attribute `markdown=1`. The rule given above seems a simpler and + // more elegant way of achieving the same expressive power, which is also + // much simpler to parse. + // + // The main potential drawback is that one can no longer paste HTML + // blocks into Markdown documents with 100% reliability. However, + // *in most cases* this will work fine, because the blank lines in + // HTML are usually followed by HTML block tags. For example: + [Test] + public void LeafBlocksHTMLBlocks_Example159() + { + // Example 159 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + //
+ // + // + // + // + // + // + // + //
+ // Hi + //
+ // + // Should be rendered as: + // + // + // + // + //
+ // Hi + //
+ + Console.WriteLine("Example 159\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n\n\n\n\n\n\n\n
\nHi\n
", "\n\n\n\n
\nHi\n
", ""); + } + + // There are problems, however, if the inner tags are indented + // *and* separated by spaces, as then they will be interpreted as + // an indented code block: + [Test] + public void LeafBlocksHTMLBlocks_Example160() + { + // Example 160 + // Section: Leaf blocks / HTML blocks + // + // The following Markdown: + // + // + // + // + // + // + // + // + //
+ // Hi + //
+ // + // Should be rendered as: + // + // + //
<td>
+            //       Hi
+            //     </td>
+            //     
+ // + //
+ + Console.WriteLine("Example 160\nSection Leaf blocks / HTML blocks\n"); + TestRoundtrip.TestSpec("\n\n \n\n \n\n \n\n
\n Hi\n
", "\n \n
<td>\n  Hi\n</td>\n
\n \n
", ""); + } + } + + [TestFixture] + public class TestLeafBlocksLinkReferenceDefinitions + { + // Fortunately, blank lines are usually not necessary and can be + // deleted. The exception is inside `
` tags, but as described
+        // [above][HTML blocks], raw HTML blocks starting with `
`
+        // *can* contain blank lines.
+        // 
+        // ## Link reference definitions
+        // 
+        // A [link reference definition](@)
+        // consists of a [link label], indented up to three spaces, followed
+        // by a colon (`:`), optional [whitespace] (including up to one
+        // [line ending]), a [link destination],
+        // optional [whitespace] (including up to one
+        // [line ending]), and an optional [link
+        // title], which if it is present must be separated
+        // from the [link destination] by [whitespace].
+        // No further [non-whitespace characters] may occur on the line.
+        // 
+        // A [link reference definition]
+        // does not correspond to a structural element of a document.  Instead, it
+        // defines a label which can be used in [reference links]
+        // and reference-style [images] elsewhere in the document.  [Link
+        // reference definitions] can come either before or after the links that use
+        // them.
+        [Test]
+        public void LeafBlocksLinkReferenceDefinitions_Example161()
+        {
+            // Example 161
+            // Section: Leaf blocks / Link reference definitions
+            //
+            // The following Markdown:
+            //     [foo]: /url "title"
+            //     
+            //     [foo]
+            //
+            // Should be rendered as:
+            //     

foo

+ + Console.WriteLine("Example 161\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url \"title\"\n\n[foo]", "

foo

", ""); + } + + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example162() + { + // Example 162 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: + // /url + // 'the title' + // + // [foo] + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 162\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec(" [foo]: \n /url \n 'the title' \n\n[foo]", "

foo

", ""); + } + + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example163() + { + // Example 163 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [Foo*bar\]]:my_(url) 'title (with parens)' + // + // [Foo*bar\]] + // + // Should be rendered as: + //

Foo*bar]

+ + Console.WriteLine("Example 163\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]", "

Foo*bar]

", ""); + } + + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example164() + { + // Example 164 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [Foo bar]: + // + // 'title' + // + // [Foo bar] + // + // Should be rendered as: + //

Foo bar

+ + Console.WriteLine("Example 164\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[Foo bar]:\n\n'title'\n\n[Foo bar]", "

Foo bar

", ""); + } + + // The title may extend over multiple lines: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example165() + { + // Example 165 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url ' + // title + // line1 + // line2 + // ' + // + // [foo] + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 165\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]", "

foo

", ""); + } + + // However, it may not contain a [blank line]: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example166() + { + // Example 166 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url 'title + // + // with blank line' + // + // [foo] + // + // Should be rendered as: + //

[foo]: /url 'title

+ //

with blank line'

+ //

[foo]

+ + Console.WriteLine("Example 166\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url 'title\n\nwith blank line'\n\n[foo]", "

[foo]: /url 'title

\n

with blank line'

\n

[foo]

", ""); + } + + // The title may be omitted: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example167() + { + // Example 167 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: + // /url + // + // [foo] + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 167\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]:\n/url\n\n[foo]", "

foo

", ""); + } + + // The link destination may not be omitted: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example168() + { + // Example 168 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: + // + // [foo] + // + // Should be rendered as: + //

[foo]:

+ //

[foo]

+ + Console.WriteLine("Example 168\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]:\n\n[foo]", "

[foo]:

\n

[foo]

", ""); + } + + // However, an empty link destination may be specified using + // angle brackets: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example169() + { + // Example 169 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: <> + // + // [foo] + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 169\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: <>\n\n[foo]", "

foo

", ""); + } + + // The title must be separated from the link destination by + // whitespace: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example170() + { + // Example 170 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: (baz) + // + // [foo] + // + // Should be rendered as: + //

[foo]: (baz)

+ //

[foo]

+ + Console.WriteLine("Example 170\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: (baz)\n\n[foo]", "

[foo]: (baz)

\n

[foo]

", ""); + } + + // Both title and destination can contain backslash escapes + // and literal backslashes: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example171() + { + // Example 171 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url\bar\*baz "foo\"bar\baz" + // + // [foo] + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 171\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]", "

foo

", ""); + } + + // A link can come before its corresponding definition: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example172() + { + // Example 172 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo] + // + // [foo]: url + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 172\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]\n\n[foo]: url", "

foo

", ""); + } + + // If there are several matching definitions, the first one takes + // precedence: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example173() + { + // Example 173 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo] + // + // [foo]: first + // [foo]: second + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 173\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]\n\n[foo]: first\n[foo]: second", "

foo

", ""); + } + + // As noted in the section on [Links], matching of labels is + // case-insensitive (see [matches]). + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example174() + { + // Example 174 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [FOO]: /url + // + // [Foo] + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 174\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[FOO]: /url\n\n[Foo]", "

Foo

", ""); + } + + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example175() + { + // Example 175 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [ΑΓΩ]: /φου + // + // [αγω] + // + // Should be rendered as: + //

αγω

+ + Console.WriteLine("Example 175\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[ΑΓΩ]: /φου\n\n[αγω]", "

αγω

", ""); + } + + // Here is a link reference definition with no corresponding link. + // It contributes nothing to the document. + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example176() + { + // Example 176 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url + // + // Should be rendered as: + // + Console.WriteLine("Example 176\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url", "", ""); + } + + // Here is another one: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example177() + { + // Example 177 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [ + // foo + // ]: /url + // bar + // + // Should be rendered as: + //

bar

+ + Console.WriteLine("Example 177\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[\nfoo\n]: /url\nbar", "

bar

", ""); + } + + // This is not a link reference definition, because there are + // [non-whitespace characters] after the title: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example178() + { + // Example 178 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url "title" ok + // + // Should be rendered as: + //

[foo]: /url "title" ok

+ + Console.WriteLine("Example 178\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url \"title\" ok", "

[foo]: /url "title" ok

", ""); + } + + // This is a link reference definition, but it has no title: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example179() + { + // Example 179 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url + // "title" ok + // + // Should be rendered as: + //

"title" ok

+ + Console.WriteLine("Example 179\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url\n\"title\" ok", "

"title" ok

", ""); + } + + // This is not a link reference definition, because it is indented + // four spaces: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example180() + { + // Example 180 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url "title" + // + // [foo] + // + // Should be rendered as: + //
[foo]: /url "title"
+            //     
+ //

[foo]

+ + Console.WriteLine("Example 180\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec(" [foo]: /url \"title\"\n\n[foo]", "
[foo]: /url "title"\n
\n

[foo]

", ""); + } + + // This is not a link reference definition, because it occurs inside + // a code block: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example181() + { + // Example 181 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // ``` + // [foo]: /url + // ``` + // + // [foo] + // + // Should be rendered as: + //
[foo]: /url
+            //     
+ //

[foo]

+ + Console.WriteLine("Example 181\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("```\n[foo]: /url\n```\n\n[foo]", "
[foo]: /url\n
\n

[foo]

", ""); + } + + // A [link reference definition] cannot interrupt a paragraph. + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example182() + { + // Example 182 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // Foo + // [bar]: /baz + // + // [bar] + // + // Should be rendered as: + //

Foo + // [bar]: /baz

+ //

[bar]

+ + Console.WriteLine("Example 182\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("Foo\n[bar]: /baz\n\n[bar]", "

Foo\n[bar]: /baz

\n

[bar]

", ""); + } + + // However, it can directly follow other block elements, such as headings + // and thematic breaks, and it need not be followed by a blank line. + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example183() + { + // Example 183 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // # [Foo] + // [foo]: /url + // > bar + // + // Should be rendered as: + //

Foo

+ //
+ //

bar

+ //
+ + Console.WriteLine("Example 183\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("# [Foo]\n[foo]: /url\n> bar", "

Foo

\n
\n

bar

\n
", ""); + } + + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example184() + { + // Example 184 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url + // bar + // === + // [foo] + // + // Should be rendered as: + //

bar

+ //

foo

+ + Console.WriteLine("Example 184\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url\nbar\n===\n[foo]", "

bar

\n

foo

", ""); + } + + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example185() + { + // Example 185 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url + // === + // [foo] + // + // Should be rendered as: + //

=== + // foo

+ + Console.WriteLine("Example 185\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url\n===\n[foo]", "

===\nfoo

", ""); + } + + // Several [link reference definitions] + // can occur one after another, without intervening blank lines. + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example186() + { + // Example 186 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /foo-url "foo" + // [bar]: /bar-url + // "bar" + // [baz]: /baz-url + // + // [foo], + // [bar], + // [baz] + // + // Should be rendered as: + //

foo, + // bar, + // baz

+ + Console.WriteLine("Example 186\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]", "

foo,\nbar,\nbaz

", ""); + } + + // [Link reference definitions] can occur + // inside block containers, like lists and block quotations. They + // affect the entire document, not just the container in which they + // are defined: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example187() + { + // Example 187 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo] + // + // > [foo]: /url + // + // Should be rendered as: + //

foo

+ //
+ //
+ + Console.WriteLine("Example 187\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]\n\n> [foo]: /url", "

foo

\n
\n
", ""); + } + + // Whether something is a [link reference definition] is + // independent of whether the link reference it defines is + // used in the document. Thus, for example, the following + // document contains just a link reference definition, and + // no visible content: + [Test] + public void LeafBlocksLinkReferenceDefinitions_Example188() + { + // Example 188 + // Section: Leaf blocks / Link reference definitions + // + // The following Markdown: + // [foo]: /url + // + // Should be rendered as: + // + Console.WriteLine("Example 188\nSection Leaf blocks / Link reference definitions\n"); + TestRoundtrip.TestSpec("[foo]: /url", "", ""); + } + } + + [TestFixture] + public class TestLeafBlocksParagraphs + { + // ## Paragraphs + // + // A sequence of non-blank lines that cannot be interpreted as other + // kinds of blocks forms a [paragraph](@). + // The contents of the paragraph are the result of parsing the + // paragraph's raw content as inlines. The paragraph's raw content + // is formed by concatenating the lines and removing initial and final + // [whitespace]. + // + // A simple example with two paragraphs: + [Test] + public void LeafBlocksParagraphs_Example189() + { + // Example 189 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // + // bbb + // + // Should be rendered as: + //

aaa

+ //

bbb

+ + Console.WriteLine("Example 189\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec("aaa\n\nbbb", "

aaa

\n

bbb

", ""); + } + + // Paragraphs can contain multiple lines, but no blank lines: + [Test] + public void LeafBlocksParagraphs_Example190() + { + // Example 190 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // bbb + // + // ccc + // ddd + // + // Should be rendered as: + //

aaa + // bbb

+ //

ccc + // ddd

+ + Console.WriteLine("Example 190\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec("aaa\nbbb\n\nccc\nddd", "

aaa\nbbb

\n

ccc\nddd

", ""); + } + + // Multiple blank lines between paragraph have no effect: + [Test] + public void LeafBlocksParagraphs_Example191() + { + // Example 191 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // + // + // bbb + // + // Should be rendered as: + //

aaa

+ //

bbb

+ + Console.WriteLine("Example 191\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec("aaa\n\n\nbbb", "

aaa

\n

bbb

", ""); + } + + // Leading spaces are skipped: + [Test] + public void LeafBlocksParagraphs_Example192() + { + // Example 192 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // bbb + // + // Should be rendered as: + //

aaa + // bbb

+ + Console.WriteLine("Example 192\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec(" aaa\n bbb", "

aaa\nbbb

", ""); + } + + // Lines after the first may be indented any amount, since indented + // code blocks cannot interrupt paragraphs. + [Test] + public void LeafBlocksParagraphs_Example193() + { + // Example 193 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // bbb + // ccc + // + // Should be rendered as: + //

aaa + // bbb + // ccc

+ + Console.WriteLine("Example 193\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec("aaa\n bbb\n ccc", "

aaa\nbbb\nccc

", ""); + } + + // However, the first line may be indented at most three spaces, + // or an indented code block will be triggered: + [Test] + public void LeafBlocksParagraphs_Example194() + { + // Example 194 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // bbb + // + // Should be rendered as: + //

aaa + // bbb

+ + Console.WriteLine("Example 194\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec(" aaa\nbbb", "

aaa\nbbb

", ""); + } + + [Test] + public void LeafBlocksParagraphs_Example195() + { + // Example 195 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // bbb + // + // Should be rendered as: + //
aaa
+            //     
+ //

bbb

+ + Console.WriteLine("Example 195\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec(" aaa\nbbb", "
aaa\n
\n

bbb

", ""); + } + + // Final spaces are stripped before inline parsing, so a paragraph + // that ends with two or more spaces will not end with a [hard line + // break]: + [Test] + public void LeafBlocksParagraphs_Example196() + { + // Example 196 + // Section: Leaf blocks / Paragraphs + // + // The following Markdown: + // aaa + // bbb + // + // Should be rendered as: + //

aaa
+ // bbb

+ + Console.WriteLine("Example 196\nSection Leaf blocks / Paragraphs\n"); + TestRoundtrip.TestSpec("aaa \nbbb ", "

aaa
\nbbb

", ""); + } + } + + [TestFixture] + public class TestLeafBlocksBlankLines + { + // ## Blank lines + // + // [Blank lines] between block-level elements are ignored, + // except for the role they play in determining whether a [list] + // is [tight] or [loose]. + // + // Blank lines at the beginning and end of the document are also ignored. + [Test] + public void LeafBlocksBlankLines_Example197() + { + // Example 197 + // Section: Leaf blocks / Blank lines + // + // The following Markdown: + // + // + // aaa + // + // + // # aaa + // + // + // + // Should be rendered as: + //

aaa

+ //

aaa

+ + Console.WriteLine("Example 197\nSection Leaf blocks / Blank lines\n"); + TestRoundtrip.TestSpec(" \n\naaa\n \n\n# aaa\n\n ", "

aaa

\n

aaa

", ""); + } + } + + [TestFixture] + public class TestContainerBlocksBlockQuotes + { + // # Container blocks + // + // A [container block](#container-blocks) is a block that has other + // blocks as its contents. There are two basic kinds of container blocks: + // [block quotes] and [list items]. + // [Lists] are meta-containers for [list items]. + // + // We define the syntax for container blocks recursively. The general + // form of the definition is: + // + // > If X is a sequence of blocks, then the result of + // > transforming X in such-and-such a way is a container of type Y + // > with these blocks as its content. + // + // So, we explain what counts as a block quote or list item by explaining + // how these can be *generated* from their contents. This should suffice + // to define the syntax, although it does not give a recipe for *parsing* + // these constructions. (A recipe is provided below in the section entitled + // [A parsing strategy](#appendix-a-parsing-strategy).) + // + // ## Block quotes + // + // A [block quote marker](@) + // consists of 0-3 spaces of initial indent, plus (a) the character `>` together + // with a following space, or (b) a single character `>` not followed by a space. + // + // The following rules define [block quotes]: + // + // 1. **Basic case.** If a string of lines *Ls* constitute a sequence + // of blocks *Bs*, then the result of prepending a [block quote + // marker] to the beginning of each line in *Ls* + // is a [block quote](#block-quotes) containing *Bs*. + // + // 2. **Laziness.** If a string of lines *Ls* constitute a [block + // quote](#block-quotes) with contents *Bs*, then the result of deleting + // the initial [block quote marker] from one or + // more lines in which the next [non-whitespace character] after the [block + // quote marker] is [paragraph continuation + // text] is a block quote with *Bs* as its content. + // [Paragraph continuation text](@) is text + // that will be parsed as part of the content of a paragraph, but does + // not occur at the beginning of the paragraph. + // + // 3. **Consecutiveness.** A document cannot contain two [block + // quotes] in a row unless there is a [blank line] between them. + // + // Nothing else counts as a [block quote](#block-quotes). + // + // Here is a simple example: + [Test] + public void ContainerBlocksBlockQuotes_Example198() + { + // Example 198 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > # Foo + // > bar + // > baz + // + // Should be rendered as: + //
+ //

Foo

+ //

bar + // baz

+ //
+ + Console.WriteLine("Example 198\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> # Foo\n> bar\n> baz", "
\n

Foo

\n

bar\nbaz

\n
", ""); + } + + // The spaces after the `>` characters can be omitted: + [Test] + public void ContainerBlocksBlockQuotes_Example199() + { + // Example 199 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // ># Foo + // >bar + // > baz + // + // Should be rendered as: + //
+ //

Foo

+ //

bar + // baz

+ //
+ + Console.WriteLine("Example 199\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("># Foo\n>bar\n> baz", "
\n

Foo

\n

bar\nbaz

\n
", ""); + } + + // The `>` characters can be indented 1-3 spaces: + [Test] + public void ContainerBlocksBlockQuotes_Example200() + { + // Example 200 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > # Foo + // > bar + // > baz + // + // Should be rendered as: + //
+ //

Foo

+ //

bar + // baz

+ //
+ + Console.WriteLine("Example 200\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec(" > # Foo\n > bar\n > baz", "
\n

Foo

\n

bar\nbaz

\n
", ""); + } + + // Four spaces gives us a code block: + [Test] + public void ContainerBlocksBlockQuotes_Example201() + { + // Example 201 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > # Foo + // > bar + // > baz + // + // Should be rendered as: + //
> # Foo
+            //     > bar
+            //     > baz
+            //     
+ + Console.WriteLine("Example 201\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec(" > # Foo\n > bar\n > baz", "
> # Foo\n> bar\n> baz\n
", ""); + } + + // The Laziness clause allows us to omit the `>` before + // [paragraph continuation text]: + [Test] + public void ContainerBlocksBlockQuotes_Example202() + { + // Example 202 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > # Foo + // > bar + // baz + // + // Should be rendered as: + //
+ //

Foo

+ //

bar + // baz

+ //
+ + Console.WriteLine("Example 202\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> # Foo\n> bar\nbaz", "
\n

Foo

\n

bar\nbaz

\n
", ""); + } + + // A block quote can contain some lazy and some non-lazy + // continuation lines: + [Test] + public void ContainerBlocksBlockQuotes_Example203() + { + // Example 203 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > bar + // baz + // > foo + // + // Should be rendered as: + //
+ //

bar + // baz + // foo

+ //
+ + Console.WriteLine("Example 203\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> bar\nbaz\n> foo", "
\n

bar\nbaz\nfoo

\n
", ""); + } + + // Laziness only applies to lines that would have been continuations of + // paragraphs had they been prepended with [block quote markers]. + // For example, the `> ` cannot be omitted in the second line of + // + // ``` markdown + // > foo + // > --- + // ``` + // + // without changing the meaning: + [Test] + public void ContainerBlocksBlockQuotes_Example204() + { + // Example 204 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > foo + // --- + // + // Should be rendered as: + //
+ //

foo

+ //
+ //
+ + Console.WriteLine("Example 204\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> foo\n---", "
\n

foo

\n
\n
", ""); + } + + // Similarly, if we omit the `> ` in the second line of + // + // ``` markdown + // > - foo + // > - bar + // ``` + // + // then the block quote ends after the first line: + [Test] + public void ContainerBlocksBlockQuotes_Example205() + { + // Example 205 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > - foo + // - bar + // + // Should be rendered as: + //
+ //
    + //
  • foo
  • + //
+ //
+ //
    + //
  • bar
  • + //
+ + Console.WriteLine("Example 205\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> - foo\n- bar", "
\n
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
", ""); + } + + // For the same reason, we can't omit the `> ` in front of + // subsequent lines of an indented or fenced code block: + [Test] + public void ContainerBlocksBlockQuotes_Example206() + { + // Example 206 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > foo + // bar + // + // Should be rendered as: + //
+ //
foo
+            //     
+ //
+ //
bar
+            //     
+ + Console.WriteLine("Example 206\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> foo\n bar", "
\n
foo\n
\n
\n
bar\n
", ""); + } + + [Test] + public void ContainerBlocksBlockQuotes_Example207() + { + // Example 207 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > ``` + // foo + // ``` + // + // Should be rendered as: + //
+ //
+ //
+ //

foo

+ //
+ + Console.WriteLine("Example 207\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> ```\nfoo\n```", "
\n
\n
\n

foo

\n
", ""); + } + + // Note that in the following case, we have a [lazy + // continuation line]: + [Test] + public void ContainerBlocksBlockQuotes_Example208() + { + // Example 208 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > foo + // - bar + // + // Should be rendered as: + //
+ //

foo + // - bar

+ //
+ + Console.WriteLine("Example 208\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> foo\n - bar", "
\n

foo\n- bar

\n
", ""); + } + + // To see why, note that in + // + // ```markdown + // > foo + // > - bar + // ``` + // + // the `- bar` is indented too far to start a list, and can't + // be an indented code block because indented code blocks cannot + // interrupt paragraphs, so it is [paragraph continuation text]. + // + // A block quote can be empty: + [Test] + public void ContainerBlocksBlockQuotes_Example209() + { + // Example 209 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > + // + // Should be rendered as: + //
+ //
+ + Console.WriteLine("Example 209\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec(">", "
\n
", ""); + } + + [Test] + public void ContainerBlocksBlockQuotes_Example210() + { + // Example 210 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > + // > + // > + // + // Should be rendered as: + //
+ //
+ + Console.WriteLine("Example 210\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec(">\n> \n> ", "
\n
", ""); + } + + // A block quote can have initial or final blank lines: + [Test] + public void ContainerBlocksBlockQuotes_Example211() + { + // Example 211 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > + // > foo + // > + // + // Should be rendered as: + //
+ //

foo

+ //
+ + Console.WriteLine("Example 211\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec(">\n> foo\n> ", "
\n

foo

\n
", ""); + } + + // A blank line always separates block quotes: + [Test] + public void ContainerBlocksBlockQuotes_Example212() + { + // Example 212 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > foo + // + // > bar + // + // Should be rendered as: + //
+ //

foo

+ //
+ //
+ //

bar

+ //
+ + Console.WriteLine("Example 212\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> foo\n\n> bar", "
\n

foo

\n
\n
\n

bar

\n
", ""); + } + + // (Most current Markdown implementations, including John Gruber's + // original `Markdown.pl`, will parse this example as a single block quote + // with two paragraphs. But it seems better to allow the author to decide + // whether two block quotes or one are wanted.) + // + // Consecutiveness means that if we put these block quotes together, + // we get a single block quote: + [Test] + public void ContainerBlocksBlockQuotes_Example213() + { + // Example 213 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > foo + // > bar + // + // Should be rendered as: + //
+ //

foo + // bar

+ //
+ + Console.WriteLine("Example 213\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> foo\n> bar", "
\n

foo\nbar

\n
", ""); + } + + // To get a block quote with two paragraphs, use: + [Test] + public void ContainerBlocksBlockQuotes_Example214() + { + // Example 214 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > foo + // > + // > bar + // + // Should be rendered as: + //
+ //

foo

+ //

bar

+ //
+ + Console.WriteLine("Example 214\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> foo\n>\n> bar", "
\n

foo

\n

bar

\n
", ""); + } + + // Block quotes can interrupt paragraphs: + [Test] + public void ContainerBlocksBlockQuotes_Example215() + { + // Example 215 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // foo + // > bar + // + // Should be rendered as: + //

foo

+ //
+ //

bar

+ //
+ + Console.WriteLine("Example 215\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("foo\n> bar", "

foo

\n
\n

bar

\n
", ""); + } + + // In general, blank lines are not needed before or after block + // quotes: + [Test] + public void ContainerBlocksBlockQuotes_Example216() + { + // Example 216 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > aaa + // *** + // > bbb + // + // Should be rendered as: + //
+ //

aaa

+ //
+ //
+ //
+ //

bbb

+ //
+ + Console.WriteLine("Example 216\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> aaa\n***\n> bbb", "
\n

aaa

\n
\n
\n
\n

bbb

\n
", ""); + } + + // However, because of laziness, a blank line is needed between + // a block quote and a following paragraph: + [Test] + public void ContainerBlocksBlockQuotes_Example217() + { + // Example 217 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > bar + // baz + // + // Should be rendered as: + //
+ //

bar + // baz

+ //
+ + Console.WriteLine("Example 217\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> bar\nbaz", "
\n

bar\nbaz

\n
", ""); + } + + [Test] + public void ContainerBlocksBlockQuotes_Example218() + { + // Example 218 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > bar + // + // baz + // + // Should be rendered as: + //
+ //

bar

+ //
+ //

baz

+ + Console.WriteLine("Example 218\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> bar\n\nbaz", "
\n

bar

\n
\n

baz

", ""); + } + + [Test] + public void ContainerBlocksBlockQuotes_Example219() + { + // Example 219 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > bar + // > + // baz + // + // Should be rendered as: + //
+ //

bar

+ //
+ //

baz

+ + Console.WriteLine("Example 219\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> bar\n>\nbaz", "
\n

bar

\n
\n

baz

", ""); + } + + // It is a consequence of the Laziness rule that any number + // of initial `>`s may be omitted on a continuation line of a + // nested block quote: + [Test] + public void ContainerBlocksBlockQuotes_Example220() + { + // Example 220 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > > > foo + // bar + // + // Should be rendered as: + //
+ //
+ //
+ //

foo + // bar

+ //
+ //
+ //
+ + Console.WriteLine("Example 220\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> > > foo\nbar", "
\n
\n
\n

foo\nbar

\n
\n
\n
", ""); + } + + [Test] + public void ContainerBlocksBlockQuotes_Example221() + { + // Example 221 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // >>> foo + // > bar + // >>baz + // + // Should be rendered as: + //
+ //
+ //
+ //

foo + // bar + // baz

+ //
+ //
+ //
+ + Console.WriteLine("Example 221\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec(">>> foo\n> bar\n>>baz", "
\n
\n
\n

foo\nbar\nbaz

\n
\n
\n
", ""); + } + + // When including an indented code block in a block quote, + // remember that the [block quote marker] includes + // both the `>` and a following space. So *five spaces* are needed after + // the `>`: + [Test] + public void ContainerBlocksBlockQuotes_Example222() + { + // Example 222 + // Section: Container blocks / Block quotes + // + // The following Markdown: + // > code + // + // > not code + // + // Should be rendered as: + //
+ //
code
+            //     
+ //
+ //
+ //

not code

+ //
+ + Console.WriteLine("Example 222\nSection Container blocks / Block quotes\n"); + TestRoundtrip.TestSpec("> code\n\n> not code", "
\n
code\n
\n
\n
\n

not code

\n
", ""); + } + } + + [TestFixture] + public class TestContainerBlocksListItems + { + // ## List items + // + // A [list marker](@) is a + // [bullet list marker] or an [ordered list marker]. + // + // A [bullet list marker](@) + // is a `-`, `+`, or `*` character. + // + // An [ordered list marker](@) + // is a sequence of 1--9 arabic digits (`0-9`), followed by either a + // `.` character or a `)` character. (The reason for the length + // limit is that with 10 digits we start seeing integer overflows + // in some browsers.) + // + // The following rules define [list items]: + // + // 1. **Basic case.** If a sequence of lines *Ls* constitute a sequence of + // blocks *Bs* starting with a [non-whitespace character], and *M* is a + // list marker of width *W* followed by 1 ≤ *N* ≤ 4 spaces, then the result + // of prepending *M* and the following spaces to the first line of + // *Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a + // list item with *Bs* as its contents. The type of the list item + // (bullet or ordered) is determined by the type of its list marker. + // If the list item is ordered, then it is also assigned a start + // number, based on the ordered list marker. + // + // Exceptions: + // + // 1. When the first list item in a [list] interrupts + // a paragraph---that is, when it starts on a line that would + // otherwise count as [paragraph continuation text]---then (a) + // the lines *Ls* must not begin with a blank line, and (b) if + // the list item is ordered, the start number must be 1. + // 2. If any line is a [thematic break][thematic breaks] then + // that line is not a list item. + // + // For example, let *Ls* be the lines + [Test] + public void ContainerBlocksListItems_Example223() + { + // Example 223 + // Section: Container blocks / List items + // + // The following Markdown: + // A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //

A paragraph + // with two lines.

+ //
indented code
+            //     
+ //
+ //

A block quote.

+ //
+ + Console.WriteLine("Example 223\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.", "

A paragraph\nwith two lines.

\n
indented code\n
\n
\n

A block quote.

\n
", ""); + } + + // And let *M* be the marker `1.`, and *N* = 2. Then rule #1 says + // that the following is an ordered list item with start number 1, + // and the same contents as *Ls*: + [Test] + public void ContainerBlocksListItems_Example224() + { + // Example 224 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //
    + //
  1. + //

    A paragraph + // with two lines.

    + //
    indented code
    +            //     
    + //
    + //

    A block quote.

    + //
    + //
  2. + //
+ + Console.WriteLine("Example 224\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.", "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
", ""); + } + + // The most important thing to notice is that the position of + // the text after the list marker determines how much indentation + // is needed in subsequent blocks in the list item. If the list + // marker takes up two spaces, and there are three spaces between + // the list marker and the next [non-whitespace character], then blocks + // must be indented five spaces in order to fall under the list + // item. + // + // Here are some examples showing how far content must be indented to be + // put under the list item: + [Test] + public void ContainerBlocksListItems_Example225() + { + // Example 225 + // Section: Container blocks / List items + // + // The following Markdown: + // - one + // + // two + // + // Should be rendered as: + //
    + //
  • one
  • + //
+ //

two

+ + Console.WriteLine("Example 225\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- one\n\n two", "
    \n
  • one
  • \n
\n

two

", ""); + } + + [Test] + public void ContainerBlocksListItems_Example226() + { + // Example 226 + // Section: Container blocks / List items + // + // The following Markdown: + // - one + // + // two + // + // Should be rendered as: + //
    + //
  • + //

    one

    + //

    two

    + //
  • + //
+ + Console.WriteLine("Example 226\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- one\n\n two", "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example227() + { + // Example 227 + // Section: Container blocks / List items + // + // The following Markdown: + // - one + // + // two + // + // Should be rendered as: + //
    + //
  • one
  • + //
+ //
 two
+            //     
+ + Console.WriteLine("Example 227\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" - one\n\n two", "
    \n
  • one
  • \n
\n
 two\n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example228() + { + // Example 228 + // Section: Container blocks / List items + // + // The following Markdown: + // - one + // + // two + // + // Should be rendered as: + //
    + //
  • + //

    one

    + //

    two

    + //
  • + //
+ + Console.WriteLine("Example 228\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" - one\n\n two", "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
", ""); + } + + // It is tempting to think of this in terms of columns: the continuation + // blocks must be indented at least to the column of the first + // [non-whitespace character] after the list marker. However, that is not quite right. + // The spaces after the list marker determine how much relative indentation + // is needed. Which column this indentation reaches will depend on + // how the list item is embedded in other constructions, as shown by + // this example: + [Test] + public void ContainerBlocksListItems_Example229() + { + // Example 229 + // Section: Container blocks / List items + // + // The following Markdown: + // > > 1. one + // >> + // >> two + // + // Should be rendered as: + //
+ //
+ //
    + //
  1. + //

    one

    + //

    two

    + //
  2. + //
+ //
+ //
+ + Console.WriteLine("Example 229\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" > > 1. one\n>>\n>> two", "
\n
\n
    \n
  1. \n

    one

    \n

    two

    \n
  2. \n
\n
\n
", ""); + } + + // Here `two` occurs in the same column as the list marker `1.`, + // but is actually contained in the list item, because there is + // sufficient indentation after the last containing blockquote marker. + // + // The converse is also possible. In the following example, the word `two` + // occurs far to the right of the initial text of the list item, `one`, but + // it is not considered part of the list item, because it is not indented + // far enough past the blockquote marker: + [Test] + public void ContainerBlocksListItems_Example230() + { + // Example 230 + // Section: Container blocks / List items + // + // The following Markdown: + // >>- one + // >> + // > > two + // + // Should be rendered as: + //
+ //
+ //
    + //
  • one
  • + //
+ //

two

+ //
+ //
+ + Console.WriteLine("Example 230\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(">>- one\n>>\n > > two", "
\n
\n
    \n
  • one
  • \n
\n

two

\n
\n
", ""); + } + + // Note that at least one space is needed between the list marker and + // any following content, so these are not list items: + [Test] + public void ContainerBlocksListItems_Example231() + { + // Example 231 + // Section: Container blocks / List items + // + // The following Markdown: + // -one + // + // 2.two + // + // Should be rendered as: + //

-one

+ //

2.two

+ + Console.WriteLine("Example 231\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("-one\n\n2.two", "

-one

\n

2.two

", ""); + } + + // A list item may contain blocks that are separated by more than + // one blank line. + [Test] + public void ContainerBlocksListItems_Example232() + { + // Example 232 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // + // + // bar + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //

    bar

    + //
  • + //
+ + Console.WriteLine("Example 232\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n\n\n bar", "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
", ""); + } + + // A list item may contain any kind of block: + [Test] + public void ContainerBlocksListItems_Example233() + { + // Example 233 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. foo + // + // ``` + // bar + // ``` + // + // baz + // + // > bam + // + // Should be rendered as: + //
    + //
  1. + //

    foo

    + //
    bar
    +            //     
    + //

    baz

    + //
    + //

    bam

    + //
    + //
  2. + //
+ + Console.WriteLine("Example 233\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam", "
    \n
  1. \n

    foo

    \n
    bar\n
    \n

    baz

    \n
    \n

    bam

    \n
    \n
  2. \n
", ""); + } + + // A list item that contains an indented code block will preserve + // empty lines within the code block verbatim. + [Test] + public void ContainerBlocksListItems_Example234() + { + // Example 234 + // Section: Container blocks / List items + // + // The following Markdown: + // - Foo + // + // bar + // + // + // baz + // + // Should be rendered as: + //
    + //
  • + //

    Foo

    + //
    bar
    +            //     
    +            //     
    +            //     baz
    +            //     
    + //
  • + //
+ + Console.WriteLine("Example 234\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- Foo\n\n bar\n\n\n baz", "
    \n
  • \n

    Foo

    \n
    bar\n\n\nbaz\n
    \n
  • \n
", ""); + } + + // Note that ordered list start numbers must be nine digits or less: + [Test] + public void ContainerBlocksListItems_Example235() + { + // Example 235 + // Section: Container blocks / List items + // + // The following Markdown: + // 123456789. ok + // + // Should be rendered as: + //
    + //
  1. ok
  2. + //
+ + Console.WriteLine("Example 235\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("123456789. ok", "
    \n
  1. ok
  2. \n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example236() + { + // Example 236 + // Section: Container blocks / List items + // + // The following Markdown: + // 1234567890. not ok + // + // Should be rendered as: + //

1234567890. not ok

+ + Console.WriteLine("Example 236\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1234567890. not ok", "

1234567890. not ok

", ""); + } + + // A start number may begin with 0s: + [Test] + public void ContainerBlocksListItems_Example237() + { + // Example 237 + // Section: Container blocks / List items + // + // The following Markdown: + // 0. ok + // + // Should be rendered as: + //
    + //
  1. ok
  2. + //
+ + Console.WriteLine("Example 237\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("0. ok", "
    \n
  1. ok
  2. \n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example238() + { + // Example 238 + // Section: Container blocks / List items + // + // The following Markdown: + // 003. ok + // + // Should be rendered as: + //
    + //
  1. ok
  2. + //
+ + Console.WriteLine("Example 238\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("003. ok", "
    \n
  1. ok
  2. \n
", ""); + } + + // A start number may not be negative: + [Test] + public void ContainerBlocksListItems_Example239() + { + // Example 239 + // Section: Container blocks / List items + // + // The following Markdown: + // -1. not ok + // + // Should be rendered as: + //

-1. not ok

+ + Console.WriteLine("Example 239\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("-1. not ok", "

-1. not ok

", ""); + } + + // 2. **Item starting with indented code.** If a sequence of lines *Ls* + // constitute a sequence of blocks *Bs* starting with an indented code + // block, and *M* is a list marker of width *W* followed by + // one space, then the result of prepending *M* and the following + // space to the first line of *Ls*, and indenting subsequent lines of + // *Ls* by *W + 1* spaces, is a list item with *Bs* as its contents. + // If a line is empty, then it need not be indented. The type of the + // list item (bullet or ordered) is determined by the type of its list + // marker. If the list item is ordered, then it is also assigned a + // start number, based on the ordered list marker. + // + // An indented code block will have to be indented four spaces beyond + // the edge of the region where text will be included in the list item. + // In the following case that is 6 spaces: + [Test] + public void ContainerBlocksListItems_Example240() + { + // Example 240 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // + // bar + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //
    bar
    +            //     
    + //
  • + //
+ + Console.WriteLine("Example 240\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n\n bar", "
    \n
  • \n

    foo

    \n
    bar\n
    \n
  • \n
", ""); + } + + // And in this case it is 11 spaces: + [Test] + public void ContainerBlocksListItems_Example241() + { + // Example 241 + // Section: Container blocks / List items + // + // The following Markdown: + // 10. foo + // + // bar + // + // Should be rendered as: + //
    + //
  1. + //

    foo

    + //
    bar
    +            //     
    + //
  2. + //
+ + Console.WriteLine("Example 241\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 10. foo\n\n bar", "
    \n
  1. \n

    foo

    \n
    bar\n
    \n
  2. \n
", ""); + } + + // If the *first* block in the list item is an indented code block, + // then by rule #2, the contents must be indented *one* space after the + // list marker: + [Test] + public void ContainerBlocksListItems_Example242() + { + // Example 242 + // Section: Container blocks / List items + // + // The following Markdown: + // indented code + // + // paragraph + // + // more code + // + // Should be rendered as: + //
indented code
+            //     
+ //

paragraph

+ //
more code
+            //     
+ + Console.WriteLine("Example 242\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" indented code\n\nparagraph\n\n more code", "
indented code\n
\n

paragraph

\n
more code\n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example243() + { + // Example 243 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. indented code + // + // paragraph + // + // more code + // + // Should be rendered as: + //
    + //
  1. + //
    indented code
    +            //     
    + //

    paragraph

    + //
    more code
    +            //     
    + //
  2. + //
+ + Console.WriteLine("Example 243\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1. indented code\n\n paragraph\n\n more code", "
    \n
  1. \n
    indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
", ""); + } + + // Note that an additional space indent is interpreted as space + // inside the code block: + [Test] + public void ContainerBlocksListItems_Example244() + { + // Example 244 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. indented code + // + // paragraph + // + // more code + // + // Should be rendered as: + //
    + //
  1. + //
     indented code
    +            //     
    + //

    paragraph

    + //
    more code
    +            //     
    + //
  2. + //
+ + Console.WriteLine("Example 244\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1. indented code\n\n paragraph\n\n more code", "
    \n
  1. \n
     indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
", ""); + } + + // Note that rules #1 and #2 only apply to two cases: (a) cases + // in which the lines to be included in a list item begin with a + // [non-whitespace character], and (b) cases in which + // they begin with an indented code + // block. In a case like the following, where the first block begins with + // a three-space indent, the rules do not allow us to form a list item by + // indenting the whole thing and prepending a list marker: + [Test] + public void ContainerBlocksListItems_Example245() + { + // Example 245 + // Section: Container blocks / List items + // + // The following Markdown: + // foo + // + // bar + // + // Should be rendered as: + //

foo

+ //

bar

+ + Console.WriteLine("Example 245\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" foo\n\nbar", "

foo

\n

bar

", ""); + } + + [Test] + public void ContainerBlocksListItems_Example246() + { + // Example 246 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // + // bar + // + // Should be rendered as: + //
    + //
  • foo
  • + //
+ //

bar

+ + Console.WriteLine("Example 246\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n\n bar", "
    \n
  • foo
  • \n
\n

bar

", ""); + } + + // This is not a significant restriction, because when a block begins + // with 1-3 spaces indent, the indentation can always be removed without + // a change in interpretation, allowing rule #1 to be applied. So, in + // the above case: + [Test] + public void ContainerBlocksListItems_Example247() + { + // Example 247 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // + // bar + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //

    bar

    + //
  • + //
+ + Console.WriteLine("Example 247\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n\n bar", "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
", ""); + } + + // 3. **Item starting with a blank line.** If a sequence of lines *Ls* + // starting with a single [blank line] constitute a (possibly empty) + // sequence of blocks *Bs*, not separated from each other by more than + // one blank line, and *M* is a list marker of width *W*, + // then the result of prepending *M* to the first line of *Ls*, and + // indenting subsequent lines of *Ls* by *W + 1* spaces, is a list + // item with *Bs* as its contents. + // If a line is empty, then it need not be indented. The type of the + // list item (bullet or ordered) is determined by the type of its list + // marker. If the list item is ordered, then it is also assigned a + // start number, based on the ordered list marker. + // + // Here are some list items that start with a blank line but are not empty: + [Test] + public void ContainerBlocksListItems_Example248() + { + // Example 248 + // Section: Container blocks / List items + // + // The following Markdown: + // - + // foo + // - + // ``` + // bar + // ``` + // - + // baz + // + // Should be rendered as: + //
    + //
  • foo
  • + //
  • + //
    bar
    +            //     
    + //
  • + //
  • + //
    baz
    +            //     
    + //
  • + //
+ + Console.WriteLine("Example 248\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("-\n foo\n-\n ```\n bar\n ```\n-\n baz", "
    \n
  • foo
  • \n
  • \n
    bar\n
    \n
  • \n
  • \n
    baz\n
    \n
  • \n
", ""); + } + + // When the list item starts with a blank line, the number of spaces + // following the list marker doesn't change the required indentation: + [Test] + public void ContainerBlocksListItems_Example249() + { + // Example 249 + // Section: Container blocks / List items + // + // The following Markdown: + // - + // foo + // + // Should be rendered as: + //
    + //
  • foo
  • + //
+ + Console.WriteLine("Example 249\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- \n foo", "
    \n
  • foo
  • \n
", ""); + } + + // A list item can begin with at most one blank line. + // In the following example, `foo` is not part of the list + // item: + [Test] + public void ContainerBlocksListItems_Example250() + { + // Example 250 + // Section: Container blocks / List items + // + // The following Markdown: + // - + // + // foo + // + // Should be rendered as: + //
    + //
  • + //
+ //

foo

+ + Console.WriteLine("Example 250\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("-\n\n foo", "
    \n
  • \n
\n

foo

", ""); + } + + // Here is an empty bullet list item: + [Test] + public void ContainerBlocksListItems_Example251() + { + // Example 251 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // - + // - bar + // + // Should be rendered as: + //
    + //
  • foo
  • + //
  • + //
  • bar
  • + //
+ + Console.WriteLine("Example 251\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n-\n- bar", "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
", ""); + } + + // It does not matter whether there are spaces following the [list marker]: + [Test] + public void ContainerBlocksListItems_Example252() + { + // Example 252 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // - + // - bar + // + // Should be rendered as: + //
    + //
  • foo
  • + //
  • + //
  • bar
  • + //
+ + Console.WriteLine("Example 252\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n- \n- bar", "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
", ""); + } + + // Here is an empty ordered list item: + [Test] + public void ContainerBlocksListItems_Example253() + { + // Example 253 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. foo + // 2. + // 3. bar + // + // Should be rendered as: + //
    + //
  1. foo
  2. + //
  3. + //
  4. bar
  5. + //
+ + Console.WriteLine("Example 253\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1. foo\n2.\n3. bar", "
    \n
  1. foo
  2. \n
  3. \n
  4. bar
  5. \n
", ""); + } + + // A list may start or end with an empty list item: + [Test] + public void ContainerBlocksListItems_Example254() + { + // Example 254 + // Section: Container blocks / List items + // + // The following Markdown: + // * + // + // Should be rendered as: + //
    + //
  • + //
+ + Console.WriteLine("Example 254\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("*", "
    \n
  • \n
", ""); + } + + // However, an empty list item cannot interrupt a paragraph: + [Test] + public void ContainerBlocksListItems_Example255() + { + // Example 255 + // Section: Container blocks / List items + // + // The following Markdown: + // foo + // * + // + // foo + // 1. + // + // Should be rendered as: + //

foo + // *

+ //

foo + // 1.

+ + Console.WriteLine("Example 255\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("foo\n*\n\nfoo\n1.", "

foo\n*

\n

foo\n1.

", ""); + } + + // 4. **Indentation.** If a sequence of lines *Ls* constitutes a list item + // according to rule #1, #2, or #3, then the result of indenting each line + // of *Ls* by 1-3 spaces (the same for each line) also constitutes a + // list item with the same contents and attributes. If a line is + // empty, then it need not be indented. + // + // Indented one space: + [Test] + public void ContainerBlocksListItems_Example256() + { + // Example 256 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //
    + //
  1. + //

    A paragraph + // with two lines.

    + //
    indented code
    +            //     
    + //
    + //

    A block quote.

    + //
    + //
  2. + //
+ + Console.WriteLine("Example 256\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.", "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
", ""); + } + + // Indented two spaces: + [Test] + public void ContainerBlocksListItems_Example257() + { + // Example 257 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //
    + //
  1. + //

    A paragraph + // with two lines.

    + //
    indented code
    +            //     
    + //
    + //

    A block quote.

    + //
    + //
  2. + //
+ + Console.WriteLine("Example 257\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.", "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
", ""); + } + + // Indented three spaces: + [Test] + public void ContainerBlocksListItems_Example258() + { + // Example 258 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //
    + //
  1. + //

    A paragraph + // with two lines.

    + //
    indented code
    +            //     
    + //
    + //

    A block quote.

    + //
    + //
  2. + //
+ + Console.WriteLine("Example 258\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.", "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
", ""); + } + + // Four spaces indent gives a code block: + [Test] + public void ContainerBlocksListItems_Example259() + { + // Example 259 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //
1.  A paragraph
+            //         with two lines.
+            //     
+            //             indented code
+            //     
+            //         > A block quote.
+            //     
+ + Console.WriteLine("Example 259\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.", "
1.  A paragraph\n    with two lines.\n\n        indented code\n\n    > A block quote.\n
", ""); + } + + // 5. **Laziness.** If a string of lines *Ls* constitute a [list + // item](#list-items) with contents *Bs*, then the result of deleting + // some or all of the indentation from one or more lines in which the + // next [non-whitespace character] after the indentation is + // [paragraph continuation text] is a + // list item with the same contents and attributes. The unindented + // lines are called + // [lazy continuation line](@)s. + // + // Here is an example with [lazy continuation lines]: + [Test] + public void ContainerBlocksListItems_Example260() + { + // Example 260 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // indented code + // + // > A block quote. + // + // Should be rendered as: + //
    + //
  1. + //

    A paragraph + // with two lines.

    + //
    indented code
    +            //     
    + //
    + //

    A block quote.

    + //
    + //
  2. + //
+ + Console.WriteLine("Example 260\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.", "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
", ""); + } + + // Indentation can be partially deleted: + [Test] + public void ContainerBlocksListItems_Example261() + { + // Example 261 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. A paragraph + // with two lines. + // + // Should be rendered as: + //
    + //
  1. A paragraph + // with two lines.
  2. + //
+ + Console.WriteLine("Example 261\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec(" 1. A paragraph\n with two lines.", "
    \n
  1. A paragraph\nwith two lines.
  2. \n
", ""); + } + + // These examples show how laziness can work in nested structures: + [Test] + public void ContainerBlocksListItems_Example262() + { + // Example 262 + // Section: Container blocks / List items + // + // The following Markdown: + // > 1. > Blockquote + // continued here. + // + // Should be rendered as: + //
+ //
    + //
  1. + //
    + //

    Blockquote + // continued here.

    + //
    + //
  2. + //
+ //
+ + Console.WriteLine("Example 262\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("> 1. > Blockquote\ncontinued here.", "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example263() + { + // Example 263 + // Section: Container blocks / List items + // + // The following Markdown: + // > 1. > Blockquote + // > continued here. + // + // Should be rendered as: + //
+ //
    + //
  1. + //
    + //

    Blockquote + // continued here.

    + //
    + //
  2. + //
+ //
+ + Console.WriteLine("Example 263\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("> 1. > Blockquote\n> continued here.", "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
", ""); + } + + // 6. **That's all.** Nothing that is not counted as a list item by rules + // #1--5 counts as a [list item](#list-items). + // + // The rules for sublists follow from the general rules + // [above][List items]. A sublist must be indented the same number + // of spaces a paragraph would need to be in order to be included + // in the list item. + // + // So, in this case we need two spaces indent: + [Test] + public void ContainerBlocksListItems_Example264() + { + // Example 264 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // - bar + // - baz + // - boo + // + // Should be rendered as: + //
    + //
  • foo + //
      + //
    • bar + //
        + //
      • baz + //
          + //
        • boo
        • + //
        + //
      • + //
      + //
    • + //
    + //
  • + //
+ + Console.WriteLine("Example 264\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n - bar\n - baz\n - boo", "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz\n
          \n
        • boo
        • \n
        \n
      • \n
      \n
    • \n
    \n
  • \n
", ""); + } + + // One is not enough: + [Test] + public void ContainerBlocksListItems_Example265() + { + // Example 265 + // Section: Container blocks / List items + // + // The following Markdown: + // - foo + // - bar + // - baz + // - boo + // + // Should be rendered as: + //
    + //
  • foo
  • + //
  • bar
  • + //
  • baz
  • + //
  • boo
  • + //
+ + Console.WriteLine("Example 265\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- foo\n - bar\n - baz\n - boo", "
    \n
  • foo
  • \n
  • bar
  • \n
  • baz
  • \n
  • boo
  • \n
", ""); + } + + // Here we need four, because the list marker is wider: + [Test] + public void ContainerBlocksListItems_Example266() + { + // Example 266 + // Section: Container blocks / List items + // + // The following Markdown: + // 10) foo + // - bar + // + // Should be rendered as: + //
    + //
  1. foo + //
      + //
    • bar
    • + //
    + //
  2. + //
+ + Console.WriteLine("Example 266\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("10) foo\n - bar", "
    \n
  1. foo\n
      \n
    • bar
    • \n
    \n
  2. \n
", ""); + } + + // Three is not enough: + [Test] + public void ContainerBlocksListItems_Example267() + { + // Example 267 + // Section: Container blocks / List items + // + // The following Markdown: + // 10) foo + // - bar + // + // Should be rendered as: + //
    + //
  1. foo
  2. + //
+ //
    + //
  • bar
  • + //
+ + Console.WriteLine("Example 267\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("10) foo\n - bar", "
    \n
  1. foo
  2. \n
\n
    \n
  • bar
  • \n
", ""); + } + + // A list may be the first block in a list item: + [Test] + public void ContainerBlocksListItems_Example268() + { + // Example 268 + // Section: Container blocks / List items + // + // The following Markdown: + // - - foo + // + // Should be rendered as: + //
    + //
  • + //
      + //
    • foo
    • + //
    + //
  • + //
+ + Console.WriteLine("Example 268\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- - foo", "
    \n
  • \n
      \n
    • foo
    • \n
    \n
  • \n
", ""); + } + + [Test] + public void ContainerBlocksListItems_Example269() + { + // Example 269 + // Section: Container blocks / List items + // + // The following Markdown: + // 1. - 2. foo + // + // Should be rendered as: + //
    + //
  1. + //
      + //
    • + //
        + //
      1. foo
      2. + //
      + //
    • + //
    + //
  2. + //
+ + Console.WriteLine("Example 269\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("1. - 2. foo", "
    \n
  1. \n
      \n
    • \n
        \n
      1. foo
      2. \n
      \n
    • \n
    \n
  2. \n
", ""); + } + + // A list item can contain a heading: + [Test] + public void ContainerBlocksListItems_Example270() + { + // Example 270 + // Section: Container blocks / List items + // + // The following Markdown: + // - # Foo + // - Bar + // --- + // baz + // + // Should be rendered as: + //
    + //
  • + //

    Foo

    + //
  • + //
  • + //

    Bar

    + // baz
  • + //
+ + Console.WriteLine("Example 270\nSection Container blocks / List items\n"); + TestRoundtrip.TestSpec("- # Foo\n- Bar\n ---\n baz", "
    \n
  • \n

    Foo

    \n
  • \n
  • \n

    Bar

    \nbaz
  • \n
", ""); + } + } + + [TestFixture] + public class TestContainerBlocksLists + { + // ### Motivation + // + // John Gruber's Markdown spec says the following about list items: + // + // 1. "List markers typically start at the left margin, but may be indented + // by up to three spaces. List markers must be followed by one or more + // spaces or a tab." + // + // 2. "To make lists look nice, you can wrap items with hanging indents.... + // But if you don't want to, you don't have to." + // + // 3. "List items may consist of multiple paragraphs. Each subsequent + // paragraph in a list item must be indented by either 4 spaces or one + // tab." + // + // 4. "It looks nice if you indent every line of the subsequent paragraphs, + // but here again, Markdown will allow you to be lazy." + // + // 5. "To put a blockquote within a list item, the blockquote's `>` + // delimiters need to be indented." + // + // 6. "To put a code block within a list item, the code block needs to be + // indented twice — 8 spaces or two tabs." + // + // These rules specify that a paragraph under a list item must be indented + // four spaces (presumably, from the left margin, rather than the start of + // the list marker, but this is not said), and that code under a list item + // must be indented eight spaces instead of the usual four. They also say + // that a block quote must be indented, but not by how much; however, the + // example given has four spaces indentation. Although nothing is said + // about other kinds of block-level content, it is certainly reasonable to + // infer that *all* block elements under a list item, including other + // lists, must be indented four spaces. This principle has been called the + // *four-space rule*. + // + // The four-space rule is clear and principled, and if the reference + // implementation `Markdown.pl` had followed it, it probably would have + // become the standard. However, `Markdown.pl` allowed paragraphs and + // sublists to start with only two spaces indentation, at least on the + // outer level. Worse, its behavior was inconsistent: a sublist of an + // outer-level list needed two spaces indentation, but a sublist of this + // sublist needed three spaces. It is not surprising, then, that different + // implementations of Markdown have developed very different rules for + // determining what comes under a list item. (Pandoc and python-Markdown, + // for example, stuck with Gruber's syntax description and the four-space + // rule, while discount, redcarpet, marked, PHP Markdown, and others + // followed `Markdown.pl`'s behavior more closely.) + // + // Unfortunately, given the divergences between implementations, there + // is no way to give a spec for list items that will be guaranteed not + // to break any existing documents. However, the spec given here should + // correctly handle lists formatted with either the four-space rule or + // the more forgiving `Markdown.pl` behavior, provided they are laid out + // in a way that is natural for a human to read. + // + // The strategy here is to let the width and indentation of the list marker + // determine the indentation necessary for blocks to fall under the list + // item, rather than having a fixed and arbitrary number. The writer can + // think of the body of the list item as a unit which gets indented to the + // right enough to fit the list marker (and any indentation on the list + // marker). (The laziness rule, #5, then allows continuation lines to be + // unindented if needed.) + // + // This rule is superior, we claim, to any rule requiring a fixed level of + // indentation from the margin. The four-space rule is clear but + // unnatural. It is quite unintuitive that + // + // ``` markdown + // - foo + // + // bar + // + // - baz + // ``` + // + // should be parsed as two lists with an intervening paragraph, + // + // ``` html + //
    + //
  • foo
  • + //
+ //

bar

+ //
    + //
  • baz
  • + //
+ // ``` + // + // as the four-space rule demands, rather than a single list, + // + // ``` html + //
    + //
  • + //

    foo

    + //

    bar

    + //
      + //
    • baz
    • + //
    + //
  • + //
+ // ``` + // + // The choice of four spaces is arbitrary. It can be learned, but it is + // not likely to be guessed, and it trips up beginners regularly. + // + // Would it help to adopt a two-space rule? The problem is that such + // a rule, together with the rule allowing 1--3 spaces indentation of the + // initial list marker, allows text that is indented *less than* the + // original list marker to be included in the list item. For example, + // `Markdown.pl` parses + // + // ``` markdown + // - one + // + // two + // ``` + // + // as a single list item, with `two` a continuation paragraph: + // + // ``` html + //
    + //
  • + //

    one

    + //

    two

    + //
  • + //
+ // ``` + // + // and similarly + // + // ``` markdown + // > - one + // > + // > two + // ``` + // + // as + // + // ``` html + //
+ //
    + //
  • + //

    one

    + //

    two

    + //
  • + //
+ //
+ // ``` + // + // This is extremely unintuitive. + // + // Rather than requiring a fixed indent from the margin, we could require + // a fixed indent (say, two spaces, or even one space) from the list marker (which + // may itself be indented). This proposal would remove the last anomaly + // discussed. Unlike the spec presented above, it would count the following + // as a list item with a subparagraph, even though the paragraph `bar` + // is not indented as far as the first paragraph `foo`: + // + // ``` markdown + // 10. foo + // + // bar + // ``` + // + // Arguably this text does read like a list item with `bar` as a subparagraph, + // which may count in favor of the proposal. However, on this proposal indented + // code would have to be indented six spaces after the list marker. And this + // would break a lot of existing Markdown, which has the pattern: + // + // ``` markdown + // 1. foo + // + // indented code + // ``` + // + // where the code is indented eight spaces. The spec above, by contrast, will + // parse this text as expected, since the code block's indentation is measured + // from the beginning of `foo`. + // + // The one case that needs special treatment is a list item that *starts* + // with indented code. How much indentation is required in that case, since + // we don't have a "first paragraph" to measure from? Rule #2 simply stipulates + // that in such cases, we require one space indentation from the list marker + // (and then the normal four spaces for the indented code). This will match the + // four-space rule in cases where the list marker plus its initial indentation + // takes four spaces (a common case), but diverge in other cases. + // + // ## Lists + // + // A [list](@) is a sequence of one or more + // list items [of the same type]. The list items + // may be separated by any number of blank lines. + // + // Two list items are [of the same type](@) + // if they begin with a [list marker] of the same type. + // Two list markers are of the + // same type if (a) they are bullet list markers using the same character + // (`-`, `+`, or `*`) or (b) they are ordered list numbers with the same + // delimiter (either `.` or `)`). + // + // A list is an [ordered list](@) + // if its constituent list items begin with + // [ordered list markers], and a + // [bullet list](@) if its constituent list + // items begin with [bullet list markers]. + // + // The [start number](@) + // of an [ordered list] is determined by the list number of + // its initial list item. The numbers of subsequent list items are + // disregarded. + // + // A list is [loose](@) if any of its constituent + // list items are separated by blank lines, or if any of its constituent + // list items directly contain two block-level elements with a blank line + // between them. Otherwise a list is [tight](@). + // (The difference in HTML output is that paragraphs in a loose list are + // wrapped in `

` tags, while paragraphs in a tight list are not.) + // + // Changing the bullet or ordered list delimiter starts a new list: + [Test] + public void ContainerBlocksLists_Example271() + { + // Example 271 + // Section: Container blocks / Lists + // + // The following Markdown: + // - foo + // - bar + // + baz + // + // Should be rendered as: + //

    + //
  • foo
  • + //
  • bar
  • + //
+ //
    + //
  • baz
  • + //
+ + Console.WriteLine("Example 271\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- foo\n- bar\n+ baz", "
    \n
  • foo
  • \n
  • bar
  • \n
\n
    \n
  • baz
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example272() + { + // Example 272 + // Section: Container blocks / Lists + // + // The following Markdown: + // 1. foo + // 2. bar + // 3) baz + // + // Should be rendered as: + //
    + //
  1. foo
  2. + //
  3. bar
  4. + //
+ //
    + //
  1. baz
  2. + //
+ + Console.WriteLine("Example 272\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("1. foo\n2. bar\n3) baz", "
    \n
  1. foo
  2. \n
  3. bar
  4. \n
\n
    \n
  1. baz
  2. \n
", ""); + } + + // In CommonMark, a list can interrupt a paragraph. That is, + // no blank line is needed to separate a paragraph from a following + // list: + [Test] + public void ContainerBlocksLists_Example273() + { + // Example 273 + // Section: Container blocks / Lists + // + // The following Markdown: + // Foo + // - bar + // - baz + // + // Should be rendered as: + //

Foo

+ //
    + //
  • bar
  • + //
  • baz
  • + //
+ + Console.WriteLine("Example 273\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("Foo\n- bar\n- baz", "

Foo

\n
    \n
  • bar
  • \n
  • baz
  • \n
", ""); + } + + // `Markdown.pl` does not allow this, through fear of triggering a list + // via a numeral in a hard-wrapped line: + // + // ``` markdown + // The number of windows in my house is + // 14. The number of doors is 6. + // ``` + // + // Oddly, though, `Markdown.pl` *does* allow a blockquote to + // interrupt a paragraph, even though the same considerations might + // apply. + // + // In CommonMark, we do allow lists to interrupt paragraphs, for + // two reasons. First, it is natural and not uncommon for people + // to start lists without blank lines: + // + // ``` markdown + // I need to buy + // - new shoes + // - a coat + // - a plane ticket + // ``` + // + // Second, we are attracted to a + // + // > [principle of uniformity](@): + // > if a chunk of text has a certain + // > meaning, it will continue to have the same meaning when put into a + // > container block (such as a list item or blockquote). + // + // (Indeed, the spec for [list items] and [block quotes] presupposes + // this principle.) This principle implies that if + // + // ``` markdown + // * I need to buy + // - new shoes + // - a coat + // - a plane ticket + // ``` + // + // is a list item containing a paragraph followed by a nested sublist, + // as all Markdown implementations agree it is (though the paragraph + // may be rendered without `

` tags, since the list is "tight"), + // then + // + // ``` markdown + // I need to buy + // - new shoes + // - a coat + // - a plane ticket + // ``` + // + // by itself should be a paragraph followed by a nested sublist. + // + // Since it is well established Markdown practice to allow lists to + // interrupt paragraphs inside list items, the [principle of + // uniformity] requires us to allow this outside list items as + // well. ([reStructuredText](http://docutils.sourceforge.net/rst.html) + // takes a different approach, requiring blank lines before lists + // even inside other list items.) + // + // In order to solve of unwanted lists in paragraphs with + // hard-wrapped numerals, we allow only lists starting with `1` to + // interrupt paragraphs. Thus, + [Test] + public void ContainerBlocksLists_Example274() + { + // Example 274 + // Section: Container blocks / Lists + // + // The following Markdown: + // The number of windows in my house is + // 14. The number of doors is 6. + // + // Should be rendered as: + //

The number of windows in my house is + // 14. The number of doors is 6.

+ + Console.WriteLine("Example 274\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("The number of windows in my house is\n14. The number of doors is 6.", "

The number of windows in my house is\n14. The number of doors is 6.

", ""); + } + + // We may still get an unintended result in cases like + [Test] + public void ContainerBlocksLists_Example275() + { + // Example 275 + // Section: Container blocks / Lists + // + // The following Markdown: + // The number of windows in my house is + // 1. The number of doors is 6. + // + // Should be rendered as: + //

The number of windows in my house is

+ //
    + //
  1. The number of doors is 6.
  2. + //
+ + Console.WriteLine("Example 275\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("The number of windows in my house is\n1. The number of doors is 6.", "

The number of windows in my house is

\n
    \n
  1. The number of doors is 6.
  2. \n
", ""); + } + + // but this rule should prevent most spurious list captures. + // + // There can be any number of blank lines between items: + [Test] + public void ContainerBlocksLists_Example276() + { + // Example 276 + // Section: Container blocks / Lists + // + // The following Markdown: + // - foo + // + // - bar + // + // + // - baz + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //
  • + //
  • + //

    bar

    + //
  • + //
  • + //

    baz

    + //
  • + //
+ + Console.WriteLine("Example 276\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- foo\n\n- bar\n\n\n- baz", "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
  • \n

    baz

    \n
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example277() + { + // Example 277 + // Section: Container blocks / Lists + // + // The following Markdown: + // - foo + // - bar + // - baz + // + // + // bim + // + // Should be rendered as: + //
    + //
  • foo + //
      + //
    • bar + //
        + //
      • + //

        baz

        + //

        bim

        + //
      • + //
      + //
    • + //
    + //
  • + //
+ + Console.WriteLine("Example 277\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- foo\n - bar\n - baz\n\n\n bim", "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • \n

        baz

        \n

        bim

        \n
      • \n
      \n
    • \n
    \n
  • \n
", ""); + } + + // To separate consecutive lists of the same type, or to separate a + // list from an indented code block that would otherwise be parsed + // as a subparagraph of the final list item, you can insert a blank HTML + // comment: + [Test] + public void ContainerBlocksLists_Example278() + { + // Example 278 + // Section: Container blocks / Lists + // + // The following Markdown: + // - foo + // - bar + // + // + // + // - baz + // - bim + // + // Should be rendered as: + //
    + //
  • foo
  • + //
  • bar
  • + //
+ // + //
    + //
  • baz
  • + //
  • bim
  • + //
+ + Console.WriteLine("Example 278\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- foo\n- bar\n\n\n\n- baz\n- bim", "
    \n
  • foo
  • \n
  • bar
  • \n
\n\n
    \n
  • baz
  • \n
  • bim
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example279() + { + // Example 279 + // Section: Container blocks / Lists + // + // The following Markdown: + // - foo + // + // notcode + // + // - foo + // + // + // + // code + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //

    notcode

    + //
  • + //
  • + //

    foo

    + //
  • + //
+ // + //
code
+            //     
+ + Console.WriteLine("Example 279\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- foo\n\n notcode\n\n- foo\n\n\n\n code", "
    \n
  • \n

    foo

    \n

    notcode

    \n
  • \n
  • \n

    foo

    \n
  • \n
\n\n
code\n
", ""); + } + + // List items need not be indented to the same level. The following + // list items will be treated as items at the same list level, + // since none is indented enough to belong to the previous list + // item: + [Test] + public void ContainerBlocksLists_Example280() + { + // Example 280 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // - c + // - d + // - e + // - f + // - g + // + // Should be rendered as: + //
    + //
  • a
  • + //
  • b
  • + //
  • c
  • + //
  • d
  • + //
  • e
  • + //
  • f
  • + //
  • g
  • + //
+ + Console.WriteLine("Example 280\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n - b\n - c\n - d\n - e\n - f\n- g", "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d
  • \n
  • e
  • \n
  • f
  • \n
  • g
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example281() + { + // Example 281 + // Section: Container blocks / Lists + // + // The following Markdown: + // 1. a + // + // 2. b + // + // 3. c + // + // Should be rendered as: + //
    + //
  1. + //

    a

    + //
  2. + //
  3. + //

    b

    + //
  4. + //
  5. + //

    c

    + //
  6. + //
+ + Console.WriteLine("Example 281\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("1. a\n\n 2. b\n\n 3. c", "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
  5. \n

    c

    \n
  6. \n
", ""); + } + + // Note, however, that list items may not be indented more than + // three spaces. Here `- e` is treated as a paragraph continuation + // line, because it is indented more than three spaces: + [Test] + public void ContainerBlocksLists_Example282() + { + // Example 282 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // - c + // - d + // - e + // + // Should be rendered as: + //
    + //
  • a
  • + //
  • b
  • + //
  • c
  • + //
  • d + // - e
  • + //
+ + Console.WriteLine("Example 282\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n - b\n - c\n - d\n - e", "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d\n- e
  • \n
", ""); + } + + // And here, `3. c` is treated as in indented code block, + // because it is indented four spaces and preceded by a + // blank line. + [Test] + public void ContainerBlocksLists_Example283() + { + // Example 283 + // Section: Container blocks / Lists + // + // The following Markdown: + // 1. a + // + // 2. b + // + // 3. c + // + // Should be rendered as: + //
    + //
  1. + //

    a

    + //
  2. + //
  3. + //

    b

    + //
  4. + //
+ //
3. c
+            //     
+ + Console.WriteLine("Example 283\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("1. a\n\n 2. b\n\n 3. c", "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
\n
3. c\n
", ""); + } + + // This is a loose list, because there is a blank line between + // two of the list items: + [Test] + public void ContainerBlocksLists_Example284() + { + // Example 284 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // + // - c + // + // Should be rendered as: + //
    + //
  • + //

    a

    + //
  • + //
  • + //

    b

    + //
  • + //
  • + //

    c

    + //
  • + //
+ + Console.WriteLine("Example 284\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n- b\n\n- c", "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    c

    \n
  • \n
", ""); + } + + // So is this, with a empty second item: + [Test] + public void ContainerBlocksLists_Example285() + { + // Example 285 + // Section: Container blocks / Lists + // + // The following Markdown: + // * a + // * + // + // * c + // + // Should be rendered as: + //
    + //
  • + //

    a

    + //
  • + //
  • + //
  • + //

    c

    + //
  • + //
+ + Console.WriteLine("Example 285\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("* a\n*\n\n* c", "
    \n
  • \n

    a

    \n
  • \n
  • \n
  • \n

    c

    \n
  • \n
", ""); + } + + // These are loose lists, even though there is no space between the items, + // because one of the items directly contains two block-level elements + // with a blank line between them: + [Test] + public void ContainerBlocksLists_Example286() + { + // Example 286 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // + // c + // - d + // + // Should be rendered as: + //
    + //
  • + //

    a

    + //
  • + //
  • + //

    b

    + //

    c

    + //
  • + //
  • + //

    d

    + //
  • + //
+ + Console.WriteLine("Example 286\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n- b\n\n c\n- d", "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n

    c

    \n
  • \n
  • \n

    d

    \n
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example287() + { + // Example 287 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // + // [ref]: /url + // - d + // + // Should be rendered as: + //
    + //
  • + //

    a

    + //
  • + //
  • + //

    b

    + //
  • + //
  • + //

    d

    + //
  • + //
+ + Console.WriteLine("Example 287\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n- b\n\n [ref]: /url\n- d", "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    d

    \n
  • \n
", ""); + } + + // This is a tight list, because the blank lines are in a code block: + [Test] + public void ContainerBlocksLists_Example288() + { + // Example 288 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - ``` + // b + // + // + // ``` + // - c + // + // Should be rendered as: + //
    + //
  • a
  • + //
  • + //
    b
    +            //     
    +            //     
    +            //     
    + //
  • + //
  • c
  • + //
+ + Console.WriteLine("Example 288\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n- ```\n b\n\n\n ```\n- c", "
    \n
  • a
  • \n
  • \n
    b\n\n\n
    \n
  • \n
  • c
  • \n
", ""); + } + + // This is a tight list, because the blank line is between two + // paragraphs of a sublist. So the sublist is loose while + // the outer list is tight: + [Test] + public void ContainerBlocksLists_Example289() + { + // Example 289 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // + // c + // - d + // + // Should be rendered as: + //
    + //
  • a + //
      + //
    • + //

      b

      + //

      c

      + //
    • + //
    + //
  • + //
  • d
  • + //
+ + Console.WriteLine("Example 289\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n - b\n\n c\n- d", "
    \n
  • a\n
      \n
    • \n

      b

      \n

      c

      \n
    • \n
    \n
  • \n
  • d
  • \n
", ""); + } + + // This is a tight list, because the blank line is inside the + // block quote: + [Test] + public void ContainerBlocksLists_Example290() + { + // Example 290 + // Section: Container blocks / Lists + // + // The following Markdown: + // * a + // > b + // > + // * c + // + // Should be rendered as: + //
    + //
  • a + //
    + //

    b

    + //
    + //
  • + //
  • c
  • + //
+ + Console.WriteLine("Example 290\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("* a\n > b\n >\n* c", "
    \n
  • a\n
    \n

    b

    \n
    \n
  • \n
  • c
  • \n
", ""); + } + + // This list is tight, because the consecutive block elements + // are not separated by blank lines: + [Test] + public void ContainerBlocksLists_Example291() + { + // Example 291 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // > b + // ``` + // c + // ``` + // - d + // + // Should be rendered as: + //
    + //
  • a + //
    + //

    b

    + //
    + //
    c
    +            //     
    + //
  • + //
  • d
  • + //
+ + Console.WriteLine("Example 291\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n > b\n ```\n c\n ```\n- d", "
    \n
  • a\n
    \n

    b

    \n
    \n
    c\n
    \n
  • \n
  • d
  • \n
", ""); + } + + // A single-paragraph list is tight: + [Test] + public void ContainerBlocksLists_Example292() + { + // Example 292 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // + // Should be rendered as: + //
    + //
  • a
  • + //
+ + Console.WriteLine("Example 292\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a", "
    \n
  • a
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example293() + { + // Example 293 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // + // Should be rendered as: + //
    + //
  • a + //
      + //
    • b
    • + //
    + //
  • + //
+ + Console.WriteLine("Example 293\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n - b", "
    \n
  • a\n
      \n
    • b
    • \n
    \n
  • \n
", ""); + } + + // This list is loose, because of the blank line between the + // two block elements in the list item: + [Test] + public void ContainerBlocksLists_Example294() + { + // Example 294 + // Section: Container blocks / Lists + // + // The following Markdown: + // 1. ``` + // foo + // ``` + // + // bar + // + // Should be rendered as: + //
    + //
  1. + //
    foo
    +            //     
    + //

    bar

    + //
  2. + //
+ + Console.WriteLine("Example 294\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("1. ```\n foo\n ```\n\n bar", "
    \n
  1. \n
    foo\n
    \n

    bar

    \n
  2. \n
", ""); + } + + // Here the outer list is loose, the inner list tight: + [Test] + public void ContainerBlocksLists_Example295() + { + // Example 295 + // Section: Container blocks / Lists + // + // The following Markdown: + // * foo + // * bar + // + // baz + // + // Should be rendered as: + //
    + //
  • + //

    foo

    + //
      + //
    • bar
    • + //
    + //

    baz

    + //
  • + //
+ + Console.WriteLine("Example 295\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("* foo\n * bar\n\n baz", "
    \n
  • \n

    foo

    \n
      \n
    • bar
    • \n
    \n

    baz

    \n
  • \n
", ""); + } + + [Test] + public void ContainerBlocksLists_Example296() + { + // Example 296 + // Section: Container blocks / Lists + // + // The following Markdown: + // - a + // - b + // - c + // + // - d + // - e + // - f + // + // Should be rendered as: + //
    + //
  • + //

    a

    + //
      + //
    • b
    • + //
    • c
    • + //
    + //
  • + //
  • + //

    d

    + //
      + //
    • e
    • + //
    • f
    • + //
    + //
  • + //
+ + Console.WriteLine("Example 296\nSection Container blocks / Lists\n"); + TestRoundtrip.TestSpec("- a\n - b\n - c\n\n- d\n - e\n - f", "
    \n
  • \n

    a

    \n
      \n
    • b
    • \n
    • c
    • \n
    \n
  • \n
  • \n

    d

    \n
      \n
    • e
    • \n
    • f
    • \n
    \n
  • \n
", ""); + } + } + + [TestFixture] + public class TestInlines + { + // # Inlines + // + // Inlines are parsed sequentially from the beginning of the character + // stream to the end (left to right, in left-to-right languages). + // Thus, for example, in + [Test] + public void Inlines_Example297() + { + // Example 297 + // Section: Inlines + // + // The following Markdown: + // `hi`lo` + // + // Should be rendered as: + //

hilo`

+ + Console.WriteLine("Example 297\nSection Inlines\n"); + TestRoundtrip.TestSpec("`hi`lo`", "

hilo`

", ""); + } + } + + [TestFixture] + public class TestInlinesBackslashEscapes + { + // `hi` is parsed as code, leaving the backtick at the end as a literal + // backtick. + // + // + // ## Backslash escapes + // + // Any ASCII punctuation character may be backslash-escaped: + [Test] + public void InlinesBackslashEscapes_Example298() + { + // Example 298 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~ + // + // Should be rendered as: + //

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

+ + Console.WriteLine("Example 298\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~", "

!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~

", ""); + } + + // Backslashes before other characters are treated as literal + // backslashes: + [Test] + public void InlinesBackslashEscapes_Example299() + { + // Example 299 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // \→\A\a\ \3\φ\« + // + // Should be rendered as: + //

\→\A\a\ \3\φ\«

+ + Console.WriteLine("Example 299\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("\\\t\\A\\a\\ \\3\\φ\\«", "

\\\t\\A\\a\\ \\3\\φ\\«

", ""); + } + + // Escaped characters are treated as regular characters and do + // not have their usual Markdown meanings: + [Test] + public void InlinesBackslashEscapes_Example300() + { + // Example 300 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // \*not emphasized* + // \
not a tag + // \[not a link](/foo) + // \`not code` + // 1\. not a list + // \* not a list + // \# not a heading + // \[foo]: /url "not a reference" + // \ö not a character entity + // + // Should be rendered as: + //

*not emphasized* + // <br/> not a tag + // [not a link](/foo) + // `not code` + // 1. not a list + // * not a list + // # not a heading + // [foo]: /url "not a reference" + // &ouml; not a character entity

+ + Console.WriteLine("Example 300\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\ö not a character entity", "

*not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"\n&ouml; not a character entity

", ""); + } + + // If a backslash is itself escaped, the following character is not: + [Test] + public void InlinesBackslashEscapes_Example301() + { + // Example 301 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // \\*emphasis* + // + // Should be rendered as: + //

\emphasis

+ + Console.WriteLine("Example 301\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("\\\\*emphasis*", "

\\emphasis

", ""); + } + + // A backslash at the end of the line is a [hard line break]: + [Test] + public void InlinesBackslashEscapes_Example302() + { + // Example 302 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // foo\ + // bar + // + // Should be rendered as: + //

foo
+ // bar

+ + Console.WriteLine("Example 302\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("foo\\\nbar", "

foo
\nbar

", ""); + } + + // Backslash escapes do not work in code blocks, code spans, autolinks, or + // raw HTML: + [Test] + public void InlinesBackslashEscapes_Example303() + { + // Example 303 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // `` \[\` `` + // + // Should be rendered as: + //

\[\`

+ + Console.WriteLine("Example 303\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("`` \\[\\` ``", "

\\[\\`

", ""); + } + + [Test] + public void InlinesBackslashEscapes_Example304() + { + // Example 304 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // \[\] + // + // Should be rendered as: + //
\[\]
+            //     
+ + Console.WriteLine("Example 304\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec(" \\[\\]", "
\\[\\]\n
", ""); + } + + [Test] + public void InlinesBackslashEscapes_Example305() + { + // Example 305 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // ~~~ + // \[\] + // ~~~ + // + // Should be rendered as: + //
\[\]
+            //     
+ + Console.WriteLine("Example 305\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("~~~\n\\[\\]\n~~~", "
\\[\\]\n
", ""); + } + + [Test] + public void InlinesBackslashEscapes_Example306() + { + // Example 306 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // + // + // Should be rendered as: + //

http://example.com?find=\*

+ + Console.WriteLine("Example 306\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("", "

http://example.com?find=\\*

", ""); + } + + [Test] + public void InlinesBackslashEscapes_Example307() + { + // Example 307 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // + // + // Should be rendered as: + // + + Console.WriteLine("Example 307\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("", "", ""); + } + + // But they work in all other contexts, including URLs and link titles, + // link references, and [info strings] in [fenced code blocks]: + [Test] + public void InlinesBackslashEscapes_Example308() + { + // Example 308 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // [foo](/bar\* "ti\*tle") + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 308\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("[foo](/bar\\* \"ti\\*tle\")", "

foo

", ""); + } + + [Test] + public void InlinesBackslashEscapes_Example309() + { + // Example 309 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // [foo] + // + // [foo]: /bar\* "ti\*tle" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 309\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"", "

foo

", ""); + } + + [Test] + public void InlinesBackslashEscapes_Example310() + { + // Example 310 + // Section: Inlines / Backslash escapes + // + // The following Markdown: + // ``` foo\+bar + // foo + // ``` + // + // Should be rendered as: + //
foo
+            //     
+ + Console.WriteLine("Example 310\nSection Inlines / Backslash escapes\n"); + TestRoundtrip.TestSpec("``` foo\\+bar\nfoo\n```", "
foo\n
", ""); + } + } + + [TestFixture] + public class TestInlinesEntityAndNumericCharacterReferences + { + // ## Entity and numeric character references + // + // Valid HTML entity references and numeric character references + // can be used in place of the corresponding Unicode character, + // with the following exceptions: + // + // - Entity and character references are not recognized in code + // blocks and code spans. + // + // - Entity and character references cannot stand in place of + // special characters that define structural elements in + // CommonMark. For example, although `*` can be used + // in place of a literal `*` character, `*` cannot replace + // `*` in emphasis delimiters, bullet list markers, or thematic + // breaks. + // + // Conforming CommonMark parsers need not store information about + // whether a particular character was represented in the source + // using a Unicode character or an entity reference. + // + // [Entity references](@) consist of `&` + any of the valid + // HTML5 entity names + `;`. The + // document + // is used as an authoritative source for the valid entity + // references and their corresponding code points. + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example311() + { + // Example 311 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + //   & © Æ Ď + // ¾ ℋ ⅆ + // ∲ ≧̸ + // + // Should be rendered as: + //

  & © Æ Ď + // ¾ ℋ ⅆ + // ∲ ≧̸

+ + Console.WriteLine("Example 311\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸", "

  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸

", ""); + } + + // [Decimal numeric character + // references](@) + // consist of `&#` + a string of 1--7 arabic digits + `;`. A + // numeric character reference is parsed as the corresponding + // Unicode character. Invalid Unicode code points will be replaced by + // the REPLACEMENT CHARACTER (`U+FFFD`). For security reasons, + // the code point `U+0000` will also be replaced by `U+FFFD`. + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example312() + { + // Example 312 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // # Ӓ Ϡ � + // + // Should be rendered as: + //

# Ӓ Ϡ �

+ + Console.WriteLine("Example 312\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("# Ӓ Ϡ �", "

# Ӓ Ϡ �

", ""); + } + + // [Hexadecimal numeric character + // references](@) consist of `&#` + + // either `X` or `x` + a string of 1-6 hexadecimal digits + `;`. + // They too are parsed as the corresponding Unicode character (this + // time specified with a hexadecimal numeral instead of decimal). + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example313() + { + // Example 313 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // " ആ ಫ + // + // Should be rendered as: + //

" ആ ಫ

+ + Console.WriteLine("Example 313\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("" ആ ಫ", "

" ആ ಫ

", ""); + } + + // Here are some nonentities: + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example314() + { + // Example 314 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + //   &x; &#; &#x; + // � + // &#abcdef0; + // &ThisIsNotDefined; &hi?; + // + // Should be rendered as: + //

&nbsp &x; &#; &#x; + // &#87654321; + // &#abcdef0; + // &ThisIsNotDefined; &hi?;

+ + Console.WriteLine("Example 314\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;", "

&nbsp &x; &#; &#x;\n&#87654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;

", ""); + } + + // Although HTML5 does accept some entity references + // without a trailing semicolon (such as `©`), these are not + // recognized here, because it makes the grammar too ambiguous: + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example315() + { + // Example 315 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // © + // + // Should be rendered as: + //

&copy

+ + Console.WriteLine("Example 315\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("©", "

&copy

", ""); + } + + // Strings that are not on the list of HTML5 named entities are not + // recognized as entity references either: + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example316() + { + // Example 316 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // &MadeUpEntity; + // + // Should be rendered as: + //

&MadeUpEntity;

+ + Console.WriteLine("Example 316\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("&MadeUpEntity;", "

&MadeUpEntity;

", ""); + } + + // Entity and numeric character references are recognized in any + // context besides code spans or code blocks, including + // URLs, [link titles], and [fenced code block][] [info strings]: + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example317() + { + // Example 317 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // + // + // Should be rendered as: + // + + Console.WriteLine("Example 317\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("", "", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example318() + { + // Example 318 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // [foo](/föö "föö") + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 318\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("[foo](/föö \"föö\")", "

foo

", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example319() + { + // Example 319 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // [foo] + // + // [foo]: /föö "föö" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 319\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("[foo]\n\n[foo]: /föö \"föö\"", "

foo

", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example320() + { + // Example 320 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // ``` föö + // foo + // ``` + // + // Should be rendered as: + //
foo
+            //     
+ + Console.WriteLine("Example 320\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("``` föö\nfoo\n```", "
foo\n
", ""); + } + + // Entity and numeric character references are treated as literal + // text in code spans and code blocks: + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example321() + { + // Example 321 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // `föö` + // + // Should be rendered as: + //

f&ouml;&ouml;

+ + Console.WriteLine("Example 321\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("`föö`", "

f&ouml;&ouml;

", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example322() + { + // Example 322 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // föfö + // + // Should be rendered as: + //
f&ouml;f&ouml;
+            //     
+ + Console.WriteLine("Example 322\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec(" föfö", "
f&ouml;f&ouml;\n
", ""); + } + + // Entity and numeric character references cannot be used + // in place of symbols indicating structure in CommonMark + // documents. + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example323() + { + // Example 323 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // *foo* + // *foo* + // + // Should be rendered as: + //

*foo* + // foo

+ + Console.WriteLine("Example 323\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("*foo*\n*foo*", "

*foo*\nfoo

", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example324() + { + // Example 324 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // * foo + // + // * foo + // + // Should be rendered as: + //

* foo

+ //
    + //
  • foo
  • + //
+ + Console.WriteLine("Example 324\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("* foo\n\n* foo", "

* foo

\n
    \n
  • foo
  • \n
", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example325() + { + // Example 325 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // foo bar + // + // Should be rendered as: + //

foo + // + // bar

+ + Console.WriteLine("Example 325\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("foo bar", "

foo\n\nbar

", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example326() + { + // Example 326 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

→foo

+ + Console.WriteLine("Example 326\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec(" foo", "

\tfoo

", ""); + } + + [Test] + public void InlinesEntityAndNumericCharacterReferences_Example327() + { + // Example 327 + // Section: Inlines / Entity and numeric character references + // + // The following Markdown: + // [a](url "tit") + // + // Should be rendered as: + //

[a](url "tit")

+ + Console.WriteLine("Example 327\nSection Inlines / Entity and numeric character references\n"); + TestRoundtrip.TestSpec("[a](url "tit")", "

[a](url "tit")

", ""); + } + } + + [TestFixture] + public class TestInlinesCodeSpans + { + // ## Code spans + // + // A [backtick string](@) + // is a string of one or more backtick characters (`` ` ``) that is neither + // preceded nor followed by a backtick. + // + // A [code span](@) begins with a backtick string and ends with + // a backtick string of equal length. The contents of the code span are + // the characters between the two backtick strings, normalized in the + // following ways: + // + // - First, [line endings] are converted to [spaces]. + // - If the resulting string both begins *and* ends with a [space] + // character, but does not consist entirely of [space] + // characters, a single [space] character is removed from the + // front and back. This allows you to include code that begins + // or ends with backtick characters, which must be separated by + // whitespace from the opening or closing backtick strings. + // + // This is a simple code span: + [Test] + public void InlinesCodeSpans_Example328() + { + // Example 328 + // Section: Inlines / Code spans + // + // The following Markdown: + // `foo` + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 328\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`foo`", "

foo

", ""); + } + + // Here two backticks are used, because the code contains a backtick. + // This example also illustrates stripping of a single leading and + // trailing space: + [Test] + public void InlinesCodeSpans_Example329() + { + // Example 329 + // Section: Inlines / Code spans + // + // The following Markdown: + // `` foo ` bar `` + // + // Should be rendered as: + //

foo ` bar

+ + Console.WriteLine("Example 329\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`` foo ` bar ``", "

foo ` bar

", ""); + } + + // This example shows the motivation for stripping leading and trailing + // spaces: + [Test] + public void InlinesCodeSpans_Example330() + { + // Example 330 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` `` ` + // + // Should be rendered as: + //

``

+ + Console.WriteLine("Example 330\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("` `` `", "

``

", ""); + } + + // Note that only *one* space is stripped: + [Test] + public void InlinesCodeSpans_Example331() + { + // Example 331 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` `` ` + // + // Should be rendered as: + //

``

+ + Console.WriteLine("Example 331\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("` `` `", "

``

", ""); + } + + // The stripping only happens if the space is on both + // sides of the string: + [Test] + public void InlinesCodeSpans_Example332() + { + // Example 332 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` a` + // + // Should be rendered as: + //

a

+ + Console.WriteLine("Example 332\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("` a`", "

a

", ""); + } + + // Only [spaces], and not [unicode whitespace] in general, are + // stripped in this way: + [Test] + public void InlinesCodeSpans_Example333() + { + // Example 333 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` b ` + // + // Should be rendered as: + //

 b 

+ + Console.WriteLine("Example 333\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("` b `", "

 b 

", ""); + } + + // No stripping occurs if the code span contains only spaces: + [Test] + public void InlinesCodeSpans_Example334() + { + // Example 334 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` ` + // ` ` + // + // Should be rendered as: + //

  + //

+ + Console.WriteLine("Example 334\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("` `\n` `", "

 \n

", ""); + } + + // [Line endings] are treated like spaces: + [Test] + public void InlinesCodeSpans_Example335() + { + // Example 335 + // Section: Inlines / Code spans + // + // The following Markdown: + // `` + // foo + // bar + // baz + // `` + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 335\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("``\nfoo\nbar \nbaz\n``", "

foo bar baz

", ""); + } + + [Test] + public void InlinesCodeSpans_Example336() + { + // Example 336 + // Section: Inlines / Code spans + // + // The following Markdown: + // `` + // foo + // `` + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 336\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("``\nfoo \n``", "

foo

", ""); + } + + // Interior spaces are not collapsed: + [Test] + public void InlinesCodeSpans_Example337() + { + // Example 337 + // Section: Inlines / Code spans + // + // The following Markdown: + // `foo bar + // baz` + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 337\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`foo bar \nbaz`", "

foo bar baz

", ""); + } + + // Note that browsers will typically collapse consecutive spaces + // when rendering `` elements, so it is recommended that + // the following CSS be used: + // + // code{white-space: pre-wrap;} + // + // + // Note that backslash escapes do not work in code spans. All backslashes + // are treated literally: + [Test] + public void InlinesCodeSpans_Example338() + { + // Example 338 + // Section: Inlines / Code spans + // + // The following Markdown: + // `foo\`bar` + // + // Should be rendered as: + //

foo\bar`

+ + Console.WriteLine("Example 338\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`foo\\`bar`", "

foo\\bar`

", ""); + } + + // Backslash escapes are never needed, because one can always choose a + // string of *n* backtick characters as delimiters, where the code does + // not contain any strings of exactly *n* backtick characters. + [Test] + public void InlinesCodeSpans_Example339() + { + // Example 339 + // Section: Inlines / Code spans + // + // The following Markdown: + // ``foo`bar`` + // + // Should be rendered as: + //

foo`bar

+ + Console.WriteLine("Example 339\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("``foo`bar``", "

foo`bar

", ""); + } + + [Test] + public void InlinesCodeSpans_Example340() + { + // Example 340 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` foo `` bar ` + // + // Should be rendered as: + //

foo `` bar

+ + Console.WriteLine("Example 340\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("` foo `` bar `", "

foo `` bar

", ""); + } + + // Code span backticks have higher precedence than any other inline + // constructs except HTML tags and autolinks. Thus, for example, this is + // not parsed as emphasized text, since the second `*` is part of a code + // span: + [Test] + public void InlinesCodeSpans_Example341() + { + // Example 341 + // Section: Inlines / Code spans + // + // The following Markdown: + // *foo`*` + // + // Should be rendered as: + //

*foo*

+ + Console.WriteLine("Example 341\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("*foo`*`", "

*foo*

", ""); + } + + // And this is not parsed as a link: + [Test] + public void InlinesCodeSpans_Example342() + { + // Example 342 + // Section: Inlines / Code spans + // + // The following Markdown: + // [not a `link](/foo`) + // + // Should be rendered as: + //

[not a link](/foo)

+ + Console.WriteLine("Example 342\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("[not a `link](/foo`)", "

[not a link](/foo)

", ""); + } + + // Code spans, HTML tags, and autolinks have the same precedence. + // Thus, this is code: + [Test] + public void InlinesCodeSpans_Example343() + { + // Example 343 + // Section: Inlines / Code spans + // + // The following Markdown: + // `` + // + // Should be rendered as: + //

<a href="">`

+ + Console.WriteLine("Example 343\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`
`", "

<a href="">`

", ""); + } + + // But this is an HTML tag: + [Test] + public void InlinesCodeSpans_Example344() + { + // Example 344 + // Section: Inlines / Code spans + // + // The following Markdown: + //
` + // + // Should be rendered as: + //

`

+ + Console.WriteLine("Example 344\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`", "

`

", ""); + } + + // And this is code: + [Test] + public void InlinesCodeSpans_Example345() + { + // Example 345 + // Section: Inlines / Code spans + // + // The following Markdown: + // `` + // + // Should be rendered as: + //

<http://foo.bar.baz>`

+ + Console.WriteLine("Example 345\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("``", "

<http://foo.bar.baz>`

", ""); + } + + // But this is an autolink: + [Test] + public void InlinesCodeSpans_Example346() + { + // Example 346 + // Section: Inlines / Code spans + // + // The following Markdown: + // ` + // + // Should be rendered as: + //

http://foo.bar.`baz`

+ + Console.WriteLine("Example 346\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`", "

http://foo.bar.`baz`

", ""); + } + + // When a backtick string is not closed by a matching backtick string, + // we just have literal backticks: + [Test] + public void InlinesCodeSpans_Example347() + { + // Example 347 + // Section: Inlines / Code spans + // + // The following Markdown: + // ```foo`` + // + // Should be rendered as: + //

```foo``

+ + Console.WriteLine("Example 347\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("```foo``", "

```foo``

", ""); + } + + [Test] + public void InlinesCodeSpans_Example348() + { + // Example 348 + // Section: Inlines / Code spans + // + // The following Markdown: + // `foo + // + // Should be rendered as: + //

`foo

+ + Console.WriteLine("Example 348\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`foo", "

`foo

", ""); + } + + // The following case also illustrates the need for opening and + // closing backtick strings to be equal in length: + [Test] + public void InlinesCodeSpans_Example349() + { + // Example 349 + // Section: Inlines / Code spans + // + // The following Markdown: + // `foo``bar`` + // + // Should be rendered as: + //

`foobar

+ + Console.WriteLine("Example 349\nSection Inlines / Code spans\n"); + TestRoundtrip.TestSpec("`foo``bar``", "

`foobar

", ""); + } + } + + [TestFixture] + public class TestInlinesEmphasisAndStrongEmphasis + { + // ## Emphasis and strong emphasis + // + // John Gruber's original [Markdown syntax + // description](http://daringfireball.net/projects/markdown/syntax#em) says: + // + // > Markdown treats asterisks (`*`) and underscores (`_`) as indicators of + // > emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML + // > `` tag; double `*`'s or `_`'s will be wrapped with an HTML `` + // > tag. + // + // This is enough for most users, but these rules leave much undecided, + // especially when it comes to nested emphasis. The original + // `Markdown.pl` test suite makes it clear that triple `***` and + // `___` delimiters can be used for strong emphasis, and most + // implementations have also allowed the following patterns: + // + // ``` markdown + // ***strong emph*** + // ***strong** in emph* + // ***emph* in strong** + // **in strong *emph*** + // *in emph **strong*** + // ``` + // + // The following patterns are less widely supported, but the intent + // is clear and they are useful (especially in contexts like bibliography + // entries): + // + // ``` markdown + // *emph *with emph* in it* + // **strong **with strong** in it** + // ``` + // + // Many implementations have also restricted intraword emphasis to + // the `*` forms, to avoid unwanted emphasis in words containing + // internal underscores. (It is best practice to put these in code + // spans, but users often do not.) + // + // ``` markdown + // internal emphasis: foo*bar*baz + // no emphasis: foo_bar_baz + // ``` + // + // The rules given below capture all of these patterns, while allowing + // for efficient parsing strategies that do not backtrack. + // + // First, some definitions. A [delimiter run](@) is either + // a sequence of one or more `*` characters that is not preceded or + // followed by a non-backslash-escaped `*` character, or a sequence + // of one or more `_` characters that is not preceded or followed by + // a non-backslash-escaped `_` character. + // + // A [left-flanking delimiter run](@) is + // a [delimiter run] that is (1) not followed by [Unicode whitespace], + // and either (2a) not followed by a [punctuation character], or + // (2b) followed by a [punctuation character] and + // preceded by [Unicode whitespace] or a [punctuation character]. + // For purposes of this definition, the beginning and the end of + // the line count as Unicode whitespace. + // + // A [right-flanking delimiter run](@) is + // a [delimiter run] that is (1) not preceded by [Unicode whitespace], + // and either (2a) not preceded by a [punctuation character], or + // (2b) preceded by a [punctuation character] and + // followed by [Unicode whitespace] or a [punctuation character]. + // For purposes of this definition, the beginning and the end of + // the line count as Unicode whitespace. + // + // Here are some examples of delimiter runs. + // + // - left-flanking but not right-flanking: + // + // ``` + // ***abc + // _abc + // **"abc" + // _"abc" + // ``` + // + // - right-flanking but not left-flanking: + // + // ``` + // abc*** + // abc_ + // "abc"** + // "abc"_ + // ``` + // + // - Both left and right-flanking: + // + // ``` + // abc***def + // "abc"_"def" + // ``` + // + // - Neither left nor right-flanking: + // + // ``` + // abc *** def + // a _ b + // ``` + // + // (The idea of distinguishing left-flanking and right-flanking + // delimiter runs based on the character before and the character + // after comes from Roopesh Chander's + // [vfmd](http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags). + // vfmd uses the terminology "emphasis indicator string" instead of "delimiter + // run," and its rules for distinguishing left- and right-flanking runs + // are a bit more complex than the ones given here.) + // + // The following rules define emphasis and strong emphasis: + // + // 1. A single `*` character [can open emphasis](@) + // iff (if and only if) it is part of a [left-flanking delimiter run]. + // + // 2. A single `_` character [can open emphasis] iff + // it is part of a [left-flanking delimiter run] + // and either (a) not part of a [right-flanking delimiter run] + // or (b) part of a [right-flanking delimiter run] + // preceded by punctuation. + // + // 3. A single `*` character [can close emphasis](@) + // iff it is part of a [right-flanking delimiter run]. + // + // 4. A single `_` character [can close emphasis] iff + // it is part of a [right-flanking delimiter run] + // and either (a) not part of a [left-flanking delimiter run] + // or (b) part of a [left-flanking delimiter run] + // followed by punctuation. + // + // 5. A double `**` [can open strong emphasis](@) + // iff it is part of a [left-flanking delimiter run]. + // + // 6. A double `__` [can open strong emphasis] iff + // it is part of a [left-flanking delimiter run] + // and either (a) not part of a [right-flanking delimiter run] + // or (b) part of a [right-flanking delimiter run] + // preceded by punctuation. + // + // 7. A double `**` [can close strong emphasis](@) + // iff it is part of a [right-flanking delimiter run]. + // + // 8. A double `__` [can close strong emphasis] iff + // it is part of a [right-flanking delimiter run] + // and either (a) not part of a [left-flanking delimiter run] + // or (b) part of a [left-flanking delimiter run] + // followed by punctuation. + // + // 9. Emphasis begins with a delimiter that [can open emphasis] and ends + // with a delimiter that [can close emphasis], and that uses the same + // character (`_` or `*`) as the opening delimiter. The + // opening and closing delimiters must belong to separate + // [delimiter runs]. If one of the delimiters can both + // open and close emphasis, then the sum of the lengths of the + // delimiter runs containing the opening and closing delimiters + // must not be a multiple of 3 unless both lengths are + // multiples of 3. + // + // 10. Strong emphasis begins with a delimiter that + // [can open strong emphasis] and ends with a delimiter that + // [can close strong emphasis], and that uses the same character + // (`_` or `*`) as the opening delimiter. The + // opening and closing delimiters must belong to separate + // [delimiter runs]. If one of the delimiters can both open + // and close strong emphasis, then the sum of the lengths of + // the delimiter runs containing the opening and closing + // delimiters must not be a multiple of 3 unless both lengths + // are multiples of 3. + // + // 11. A literal `*` character cannot occur at the beginning or end of + // `*`-delimited emphasis or `**`-delimited strong emphasis, unless it + // is backslash-escaped. + // + // 12. A literal `_` character cannot occur at the beginning or end of + // `_`-delimited emphasis or `__`-delimited strong emphasis, unless it + // is backslash-escaped. + // + // Where rules 1--12 above are compatible with multiple parsings, + // the following principles resolve ambiguity: + // + // 13. The number of nestings should be minimized. Thus, for example, + // an interpretation `...` is always preferred to + // `...`. + // + // 14. An interpretation `...` is always + // preferred to `...`. + // + // 15. When two potential emphasis or strong emphasis spans overlap, + // so that the second begins before the first ends and ends after + // the first ends, the first takes precedence. Thus, for example, + // `*foo _bar* baz_` is parsed as `foo _bar baz_` rather + // than `*foo bar* baz`. + // + // 16. When there are two potential emphasis or strong emphasis spans + // with the same closing delimiter, the shorter one (the one that + // opens later) takes precedence. Thus, for example, + // `**foo **bar baz**` is parsed as `**foo bar baz` + // rather than `foo **bar baz`. + // + // 17. Inline code spans, links, images, and HTML tags group more tightly + // than emphasis. So, when there is a choice between an interpretation + // that contains one of these elements and one that does not, the + // former always wins. Thus, for example, `*[foo*](bar)` is + // parsed as `*foo*` rather than as + // `[foo](bar)`. + // + // These rules can be illustrated through a series of examples. + // + // Rule 1: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example350() + { + // Example 350 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo bar* + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 350\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo bar*", "

foo bar

", ""); + } + + // This is not emphasis, because the opening `*` is followed by + // whitespace, and hence not part of a [left-flanking delimiter run]: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example351() + { + // Example 351 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // a * foo bar* + // + // Should be rendered as: + //

a * foo bar*

+ + Console.WriteLine("Example 351\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("a * foo bar*", "

a * foo bar*

", ""); + } + + // This is not emphasis, because the opening `*` is preceded + // by an alphanumeric and followed by punctuation, and hence + // not part of a [left-flanking delimiter run]: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example352() + { + // Example 352 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // a*"foo"* + // + // Should be rendered as: + //

a*"foo"*

+ + Console.WriteLine("Example 352\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("a*\"foo\"*", "

a*"foo"*

", ""); + } + + // Unicode nonbreaking spaces count as whitespace, too: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example353() + { + // Example 353 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // * a * + // + // Should be rendered as: + //

* a *

+ + Console.WriteLine("Example 353\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("* a *", "

* a *

", ""); + } + + // Intraword emphasis with `*` is permitted: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example354() + { + // Example 354 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo*bar* + // + // Should be rendered as: + //

foobar

+ + Console.WriteLine("Example 354\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo*bar*", "

foobar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example355() + { + // Example 355 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // 5*6*78 + // + // Should be rendered as: + //

5678

+ + Console.WriteLine("Example 355\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("5*6*78", "

5678

", ""); + } + + // Rule 2: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example356() + { + // Example 356 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo bar_ + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 356\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo bar_", "

foo bar

", ""); + } + + // This is not emphasis, because the opening `_` is followed by + // whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example357() + { + // Example 357 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _ foo bar_ + // + // Should be rendered as: + //

_ foo bar_

+ + Console.WriteLine("Example 357\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_ foo bar_", "

_ foo bar_

", ""); + } + + // This is not emphasis, because the opening `_` is preceded + // by an alphanumeric and followed by punctuation: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example358() + { + // Example 358 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // a_"foo"_ + // + // Should be rendered as: + //

a_"foo"_

+ + Console.WriteLine("Example 358\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("a_\"foo\"_", "

a_"foo"_

", ""); + } + + // Emphasis with `_` is not allowed inside words: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example359() + { + // Example 359 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo_bar_ + // + // Should be rendered as: + //

foo_bar_

+ + Console.WriteLine("Example 359\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo_bar_", "

foo_bar_

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example360() + { + // Example 360 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // 5_6_78 + // + // Should be rendered as: + //

5_6_78

+ + Console.WriteLine("Example 360\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("5_6_78", "

5_6_78

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example361() + { + // Example 361 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // пристаням_стремятся_ + // + // Should be rendered as: + //

пристаням_стремятся_

+ + Console.WriteLine("Example 361\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("пристаням_стремятся_", "

пристаням_стремятся_

", ""); + } + + // Here `_` does not generate emphasis, because the first delimiter run + // is right-flanking and the second left-flanking: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example362() + { + // Example 362 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // aa_"bb"_cc + // + // Should be rendered as: + //

aa_"bb"_cc

+ + Console.WriteLine("Example 362\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("aa_\"bb\"_cc", "

aa_"bb"_cc

", ""); + } + + // This is emphasis, even though the opening delimiter is + // both left- and right-flanking, because it is preceded by + // punctuation: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example363() + { + // Example 363 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo-_(bar)_ + // + // Should be rendered as: + //

foo-(bar)

+ + Console.WriteLine("Example 363\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo-_(bar)_", "

foo-(bar)

", ""); + } + + // Rule 3: + // + // This is not emphasis, because the closing delimiter does + // not match the opening delimiter: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example364() + { + // Example 364 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo* + // + // Should be rendered as: + //

_foo*

+ + Console.WriteLine("Example 364\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo*", "

_foo*

", ""); + } + + // This is not emphasis, because the closing `*` is preceded by + // whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example365() + { + // Example 365 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo bar * + // + // Should be rendered as: + //

*foo bar *

+ + Console.WriteLine("Example 365\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo bar *", "

*foo bar *

", ""); + } + + // A newline also counts as whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example366() + { + // Example 366 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo bar + // * + // + // Should be rendered as: + //

*foo bar + // *

+ + Console.WriteLine("Example 366\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo bar\n*", "

*foo bar\n*

", ""); + } + + // This is not emphasis, because the second `*` is + // preceded by punctuation and followed by an alphanumeric + // (hence it is not part of a [right-flanking delimiter run]: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example367() + { + // Example 367 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *(*foo) + // + // Should be rendered as: + //

*(*foo)

+ + Console.WriteLine("Example 367\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*(*foo)", "

*(*foo)

", ""); + } + + // The point of this restriction is more easily appreciated + // with this example: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example368() + { + // Example 368 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *(*foo*)* + // + // Should be rendered as: + //

(foo)

+ + Console.WriteLine("Example 368\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*(*foo*)*", "

(foo)

", ""); + } + + // Intraword emphasis with `*` is allowed: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example369() + { + // Example 369 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo*bar + // + // Should be rendered as: + //

foobar

+ + Console.WriteLine("Example 369\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo*bar", "

foobar

", ""); + } + + // Rule 4: + // + // This is not emphasis, because the closing `_` is preceded by + // whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example370() + { + // Example 370 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo bar _ + // + // Should be rendered as: + //

_foo bar _

+ + Console.WriteLine("Example 370\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo bar _", "

_foo bar _

", ""); + } + + // This is not emphasis, because the second `_` is + // preceded by punctuation and followed by an alphanumeric: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example371() + { + // Example 371 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _(_foo) + // + // Should be rendered as: + //

_(_foo)

+ + Console.WriteLine("Example 371\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_(_foo)", "

_(_foo)

", ""); + } + + // This is emphasis within emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example372() + { + // Example 372 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _(_foo_)_ + // + // Should be rendered as: + //

(foo)

+ + Console.WriteLine("Example 372\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_(_foo_)_", "

(foo)

", ""); + } + + // Intraword emphasis is disallowed for `_`: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example373() + { + // Example 373 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo_bar + // + // Should be rendered as: + //

_foo_bar

+ + Console.WriteLine("Example 373\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo_bar", "

_foo_bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example374() + { + // Example 374 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _пристаням_стремятся + // + // Should be rendered as: + //

_пристаням_стремятся

+ + Console.WriteLine("Example 374\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_пристаням_стремятся", "

_пристаням_стремятся

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example375() + { + // Example 375 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo_bar_baz_ + // + // Should be rendered as: + //

foo_bar_baz

+ + Console.WriteLine("Example 375\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo_bar_baz_", "

foo_bar_baz

", ""); + } + + // This is emphasis, even though the closing delimiter is + // both left- and right-flanking, because it is followed by + // punctuation: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example376() + { + // Example 376 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _(bar)_. + // + // Should be rendered as: + //

(bar).

+ + Console.WriteLine("Example 376\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_(bar)_.", "

(bar).

", ""); + } + + // Rule 5: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example377() + { + // Example 377 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo bar** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 377\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo bar**", "

foo bar

", ""); + } + + // This is not strong emphasis, because the opening delimiter is + // followed by whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example378() + { + // Example 378 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ** foo bar** + // + // Should be rendered as: + //

** foo bar**

+ + Console.WriteLine("Example 378\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("** foo bar**", "

** foo bar**

", ""); + } + + // This is not strong emphasis, because the opening `**` is preceded + // by an alphanumeric and followed by punctuation, and hence + // not part of a [left-flanking delimiter run]: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example379() + { + // Example 379 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // a**"foo"** + // + // Should be rendered as: + //

a**"foo"**

+ + Console.WriteLine("Example 379\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("a**\"foo\"**", "

a**"foo"**

", ""); + } + + // Intraword strong emphasis with `**` is permitted: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example380() + { + // Example 380 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo**bar** + // + // Should be rendered as: + //

foobar

+ + Console.WriteLine("Example 380\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo**bar**", "

foobar

", ""); + } + + // Rule 6: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example381() + { + // Example 381 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo bar__ + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 381\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo bar__", "

foo bar

", ""); + } + + // This is not strong emphasis, because the opening delimiter is + // followed by whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example382() + { + // Example 382 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __ foo bar__ + // + // Should be rendered as: + //

__ foo bar__

+ + Console.WriteLine("Example 382\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__ foo bar__", "

__ foo bar__

", ""); + } + + // A newline counts as whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example383() + { + // Example 383 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __ + // foo bar__ + // + // Should be rendered as: + //

__ + // foo bar__

+ + Console.WriteLine("Example 383\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__\nfoo bar__", "

__\nfoo bar__

", ""); + } + + // This is not strong emphasis, because the opening `__` is preceded + // by an alphanumeric and followed by punctuation: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example384() + { + // Example 384 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // a__"foo"__ + // + // Should be rendered as: + //

a__"foo"__

+ + Console.WriteLine("Example 384\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("a__\"foo\"__", "

a__"foo"__

", ""); + } + + // Intraword strong emphasis is forbidden with `__`: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example385() + { + // Example 385 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo__bar__ + // + // Should be rendered as: + //

foo__bar__

+ + Console.WriteLine("Example 385\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo__bar__", "

foo__bar__

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example386() + { + // Example 386 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // 5__6__78 + // + // Should be rendered as: + //

5__6__78

+ + Console.WriteLine("Example 386\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("5__6__78", "

5__6__78

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example387() + { + // Example 387 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // пристаням__стремятся__ + // + // Should be rendered as: + //

пристаням__стремятся__

+ + Console.WriteLine("Example 387\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("пристаням__стремятся__", "

пристаням__стремятся__

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example388() + { + // Example 388 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo, __bar__, baz__ + // + // Should be rendered as: + //

foo, bar, baz

+ + Console.WriteLine("Example 388\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo, __bar__, baz__", "

foo, bar, baz

", ""); + } + + // This is strong emphasis, even though the opening delimiter is + // both left- and right-flanking, because it is preceded by + // punctuation: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example389() + { + // Example 389 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo-__(bar)__ + // + // Should be rendered as: + //

foo-(bar)

+ + Console.WriteLine("Example 389\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo-__(bar)__", "

foo-(bar)

", ""); + } + + // Rule 7: + // + // This is not strong emphasis, because the closing delimiter is preceded + // by whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example390() + { + // Example 390 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo bar ** + // + // Should be rendered as: + //

**foo bar **

+ + Console.WriteLine("Example 390\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo bar **", "

**foo bar **

", ""); + } + + // (Nor can it be interpreted as an emphasized `*foo bar *`, because of + // Rule 11.) + // + // This is not strong emphasis, because the second `**` is + // preceded by punctuation and followed by an alphanumeric: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example391() + { + // Example 391 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **(**foo) + // + // Should be rendered as: + //

**(**foo)

+ + Console.WriteLine("Example 391\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**(**foo)", "

**(**foo)

", ""); + } + + // The point of this restriction is more easily appreciated + // with these examples: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example392() + { + // Example 392 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *(**foo**)* + // + // Should be rendered as: + //

(foo)

+ + Console.WriteLine("Example 392\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*(**foo**)*", "

(foo)

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example393() + { + // Example 393 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **Gomphocarpus (*Gomphocarpus physocarpus*, syn. + // *Asclepias physocarpa*)** + // + // Should be rendered as: + //

Gomphocarpus (Gomphocarpus physocarpus, syn. + // Asclepias physocarpa)

+ + Console.WriteLine("Example 393\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**", "

Gomphocarpus (Gomphocarpus physocarpus, syn.\nAsclepias physocarpa)

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example394() + { + // Example 394 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo "*bar*" foo** + // + // Should be rendered as: + //

foo "bar" foo

+ + Console.WriteLine("Example 394\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo \"*bar*\" foo**", "

foo "bar" foo

", ""); + } + + // Intraword emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example395() + { + // Example 395 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo**bar + // + // Should be rendered as: + //

foobar

+ + Console.WriteLine("Example 395\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo**bar", "

foobar

", ""); + } + + // Rule 8: + // + // This is not strong emphasis, because the closing delimiter is + // preceded by whitespace: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example396() + { + // Example 396 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo bar __ + // + // Should be rendered as: + //

__foo bar __

+ + Console.WriteLine("Example 396\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo bar __", "

__foo bar __

", ""); + } + + // This is not strong emphasis, because the second `__` is + // preceded by punctuation and followed by an alphanumeric: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example397() + { + // Example 397 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __(__foo) + // + // Should be rendered as: + //

__(__foo)

+ + Console.WriteLine("Example 397\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__(__foo)", "

__(__foo)

", ""); + } + + // The point of this restriction is more easily appreciated + // with this example: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example398() + { + // Example 398 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _(__foo__)_ + // + // Should be rendered as: + //

(foo)

+ + Console.WriteLine("Example 398\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_(__foo__)_", "

(foo)

", ""); + } + + // Intraword strong emphasis is forbidden with `__`: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example399() + { + // Example 399 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo__bar + // + // Should be rendered as: + //

__foo__bar

+ + Console.WriteLine("Example 399\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo__bar", "

__foo__bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example400() + { + // Example 400 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __пристаням__стремятся + // + // Should be rendered as: + //

__пристаням__стремятся

+ + Console.WriteLine("Example 400\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__пристаням__стремятся", "

__пристаням__стремятся

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example401() + { + // Example 401 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo__bar__baz__ + // + // Should be rendered as: + //

foo__bar__baz

+ + Console.WriteLine("Example 401\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo__bar__baz__", "

foo__bar__baz

", ""); + } + + // This is strong emphasis, even though the closing delimiter is + // both left- and right-flanking, because it is followed by + // punctuation: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example402() + { + // Example 402 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __(bar)__. + // + // Should be rendered as: + //

(bar).

+ + Console.WriteLine("Example 402\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__(bar)__.", "

(bar).

", ""); + } + + // Rule 9: + // + // Any nonempty sequence of inline elements can be the contents of an + // emphasized span. + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example403() + { + // Example 403 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo [bar](/url)* + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 403\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo [bar](/url)*", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example404() + { + // Example 404 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo + // bar* + // + // Should be rendered as: + //

foo + // bar

+ + Console.WriteLine("Example 404\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo\nbar*", "

foo\nbar

", ""); + } + + // In particular, emphasis and strong emphasis can be nested + // inside emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example405() + { + // Example 405 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo __bar__ baz_ + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 405\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo __bar__ baz_", "

foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example406() + { + // Example 406 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo _bar_ baz_ + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 406\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo _bar_ baz_", "

foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example407() + { + // Example 407 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo_ bar_ + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 407\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo_ bar_", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example408() + { + // Example 408 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo *bar** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 408\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo *bar**", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example409() + { + // Example 409 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo **bar** baz* + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 409\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo **bar** baz*", "

foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example410() + { + // Example 410 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo**bar**baz* + // + // Should be rendered as: + //

foobarbaz

+ + Console.WriteLine("Example 410\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo**bar**baz*", "

foobarbaz

", ""); + } + + // Note that in the preceding case, the interpretation + // + // ``` markdown + //

foobarbaz

+ // ``` + // + // + // is precluded by the condition that a delimiter that + // can both open and close (like the `*` after `foo`) + // cannot form emphasis if the sum of the lengths of + // the delimiter runs containing the opening and + // closing delimiters is a multiple of 3 unless + // both lengths are multiples of 3. + // + // + // For the same reason, we don't get two consecutive + // emphasis sections in this example: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example411() + { + // Example 411 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo**bar* + // + // Should be rendered as: + //

foo**bar

+ + Console.WriteLine("Example 411\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo**bar*", "

foo**bar

", ""); + } + + // The same condition ensures that the following + // cases are all strong emphasis nested inside + // emphasis, even when the interior spaces are + // omitted: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example412() + { + // Example 412 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ***foo** bar* + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 412\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("***foo** bar*", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example413() + { + // Example 413 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo **bar*** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 413\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo **bar***", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example414() + { + // Example 414 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo**bar*** + // + // Should be rendered as: + //

foobar

+ + Console.WriteLine("Example 414\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo**bar***", "

foobar

", ""); + } + + // When the lengths of the interior closing and opening + // delimiter runs are *both* multiples of 3, though, + // they can match to create emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example415() + { + // Example 415 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo***bar***baz + // + // Should be rendered as: + //

foobarbaz

+ + Console.WriteLine("Example 415\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo***bar***baz", "

foobarbaz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example416() + { + // Example 416 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo******bar*********baz + // + // Should be rendered as: + //

foobar***baz

+ + Console.WriteLine("Example 416\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo******bar*********baz", "

foobar***baz

", ""); + } + + // Indefinite levels of nesting are possible: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example417() + { + // Example 417 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo **bar *baz* bim** bop* + // + // Should be rendered as: + //

foo bar baz bim bop

+ + Console.WriteLine("Example 417\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo **bar *baz* bim** bop*", "

foo bar baz bim bop

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example418() + { + // Example 418 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo [*bar*](/url)* + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 418\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo [*bar*](/url)*", "

foo bar

", ""); + } + + // There can be no empty emphasis or strong emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example419() + { + // Example 419 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ** is not an empty emphasis + // + // Should be rendered as: + //

** is not an empty emphasis

+ + Console.WriteLine("Example 419\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("** is not an empty emphasis", "

** is not an empty emphasis

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example420() + { + // Example 420 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **** is not an empty strong emphasis + // + // Should be rendered as: + //

**** is not an empty strong emphasis

+ + Console.WriteLine("Example 420\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**** is not an empty strong emphasis", "

**** is not an empty strong emphasis

", ""); + } + + // Rule 10: + // + // Any nonempty sequence of inline elements can be the contents of an + // strongly emphasized span. + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example421() + { + // Example 421 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo [bar](/url)** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 421\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo [bar](/url)**", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example422() + { + // Example 422 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo + // bar** + // + // Should be rendered as: + //

foo + // bar

+ + Console.WriteLine("Example 422\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo\nbar**", "

foo\nbar

", ""); + } + + // In particular, emphasis and strong emphasis can be nested + // inside strong emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example423() + { + // Example 423 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo _bar_ baz__ + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 423\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo _bar_ baz__", "

foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example424() + { + // Example 424 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo __bar__ baz__ + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 424\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo __bar__ baz__", "

foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example425() + { + // Example 425 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ____foo__ bar__ + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 425\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("____foo__ bar__", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example426() + { + // Example 426 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo **bar**** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 426\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo **bar****", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example427() + { + // Example 427 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo *bar* baz** + // + // Should be rendered as: + //

foo bar baz

+ + Console.WriteLine("Example 427\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo *bar* baz**", "

foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example428() + { + // Example 428 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo*bar*baz** + // + // Should be rendered as: + //

foobarbaz

+ + Console.WriteLine("Example 428\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo*bar*baz**", "

foobarbaz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example429() + { + // Example 429 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ***foo* bar** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 429\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("***foo* bar**", "

foo bar

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example430() + { + // Example 430 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo *bar*** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 430\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo *bar***", "

foo bar

", ""); + } + + // Indefinite levels of nesting are possible: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example431() + { + // Example 431 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo *bar **baz** + // bim* bop** + // + // Should be rendered as: + //

foo bar baz + // bim bop

+ + Console.WriteLine("Example 431\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo *bar **baz**\nbim* bop**", "

foo bar baz\nbim bop

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example432() + { + // Example 432 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo [*bar*](/url)** + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 432\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo [*bar*](/url)**", "

foo bar

", ""); + } + + // There can be no empty emphasis or strong emphasis: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example433() + { + // Example 433 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __ is not an empty emphasis + // + // Should be rendered as: + //

__ is not an empty emphasis

+ + Console.WriteLine("Example 433\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__ is not an empty emphasis", "

__ is not an empty emphasis

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example434() + { + // Example 434 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ____ is not an empty strong emphasis + // + // Should be rendered as: + //

____ is not an empty strong emphasis

+ + Console.WriteLine("Example 434\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("____ is not an empty strong emphasis", "

____ is not an empty strong emphasis

", ""); + } + + // Rule 11: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example435() + { + // Example 435 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo *** + // + // Should be rendered as: + //

foo ***

+ + Console.WriteLine("Example 435\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo ***", "

foo ***

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example436() + { + // Example 436 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo *\** + // + // Should be rendered as: + //

foo *

+ + Console.WriteLine("Example 436\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo *\\**", "

foo *

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example437() + { + // Example 437 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo *_* + // + // Should be rendered as: + //

foo _

+ + Console.WriteLine("Example 437\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo *_*", "

foo _

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example438() + { + // Example 438 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo ***** + // + // Should be rendered as: + //

foo *****

+ + Console.WriteLine("Example 438\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo *****", "

foo *****

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example439() + { + // Example 439 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo **\*** + // + // Should be rendered as: + //

foo *

+ + Console.WriteLine("Example 439\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo **\\***", "

foo *

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example440() + { + // Example 440 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo **_** + // + // Should be rendered as: + //

foo _

+ + Console.WriteLine("Example 440\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo **_**", "

foo _

", ""); + } + + // Note that when delimiters do not match evenly, Rule 11 determines + // that the excess literal `*` characters will appear outside of the + // emphasis, rather than inside it: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example441() + { + // Example 441 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo* + // + // Should be rendered as: + //

*foo

+ + Console.WriteLine("Example 441\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo*", "

*foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example442() + { + // Example 442 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo** + // + // Should be rendered as: + //

foo*

+ + Console.WriteLine("Example 442\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo**", "

foo*

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example443() + { + // Example 443 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ***foo** + // + // Should be rendered as: + //

*foo

+ + Console.WriteLine("Example 443\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("***foo**", "

*foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example444() + { + // Example 444 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ****foo* + // + // Should be rendered as: + //

***foo

+ + Console.WriteLine("Example 444\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("****foo*", "

***foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example445() + { + // Example 445 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo*** + // + // Should be rendered as: + //

foo*

+ + Console.WriteLine("Example 445\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo***", "

foo*

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example446() + { + // Example 446 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo**** + // + // Should be rendered as: + //

foo***

+ + Console.WriteLine("Example 446\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo****", "

foo***

", ""); + } + + // Rule 12: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example447() + { + // Example 447 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo ___ + // + // Should be rendered as: + //

foo ___

+ + Console.WriteLine("Example 447\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo ___", "

foo ___

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example448() + { + // Example 448 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo _\__ + // + // Should be rendered as: + //

foo _

+ + Console.WriteLine("Example 448\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo _\\__", "

foo _

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example449() + { + // Example 449 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo _*_ + // + // Should be rendered as: + //

foo *

+ + Console.WriteLine("Example 449\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo _*_", "

foo *

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example450() + { + // Example 450 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo _____ + // + // Should be rendered as: + //

foo _____

+ + Console.WriteLine("Example 450\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo _____", "

foo _____

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example451() + { + // Example 451 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo __\___ + // + // Should be rendered as: + //

foo _

+ + Console.WriteLine("Example 451\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo __\\___", "

foo _

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example452() + { + // Example 452 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // foo __*__ + // + // Should be rendered as: + //

foo *

+ + Console.WriteLine("Example 452\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("foo __*__", "

foo *

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example453() + { + // Example 453 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo_ + // + // Should be rendered as: + //

_foo

+ + Console.WriteLine("Example 453\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo_", "

_foo

", ""); + } + + // Note that when delimiters do not match evenly, Rule 12 determines + // that the excess literal `_` characters will appear outside of the + // emphasis, rather than inside it: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example454() + { + // Example 454 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo__ + // + // Should be rendered as: + //

foo_

+ + Console.WriteLine("Example 454\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo__", "

foo_

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example455() + { + // Example 455 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ___foo__ + // + // Should be rendered as: + //

_foo

+ + Console.WriteLine("Example 455\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("___foo__", "

_foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example456() + { + // Example 456 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ____foo_ + // + // Should be rendered as: + //

___foo

+ + Console.WriteLine("Example 456\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("____foo_", "

___foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example457() + { + // Example 457 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo___ + // + // Should be rendered as: + //

foo_

+ + Console.WriteLine("Example 457\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo___", "

foo_

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example458() + { + // Example 458 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo____ + // + // Should be rendered as: + //

foo___

+ + Console.WriteLine("Example 458\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo____", "

foo___

", ""); + } + + // Rule 13 implies that if you want emphasis nested directly inside + // emphasis, you must use different delimiters: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example459() + { + // Example 459 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo** + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 459\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo**", "

foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example460() + { + // Example 460 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *_foo_* + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 460\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*_foo_*", "

foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example461() + { + // Example 461 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __foo__ + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 461\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__foo__", "

foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example462() + { + // Example 462 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _*foo*_ + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 462\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_*foo*_", "

foo

", ""); + } + + // However, strong emphasis within strong emphasis is possible without + // switching delimiters: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example463() + { + // Example 463 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ****foo**** + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 463\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("****foo****", "

foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example464() + { + // Example 464 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ____foo____ + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 464\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("____foo____", "

foo

", ""); + } + + // Rule 13 can be applied to arbitrarily long sequences of + // delimiters: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example465() + { + // Example 465 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ******foo****** + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 465\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("******foo******", "

foo

", ""); + } + + // Rule 14: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example466() + { + // Example 466 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ***foo*** + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 466\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("***foo***", "

foo

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example467() + { + // Example 467 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _____foo_____ + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 467\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_____foo_____", "

foo

", ""); + } + + // Rule 15: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example468() + { + // Example 468 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo _bar* baz_ + // + // Should be rendered as: + //

foo _bar baz_

+ + Console.WriteLine("Example 468\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo _bar* baz_", "

foo _bar baz_

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example469() + { + // Example 469 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo __bar *baz bim__ bam* + // + // Should be rendered as: + //

foo bar *baz bim bam

+ + Console.WriteLine("Example 469\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo __bar *baz bim__ bam*", "

foo bar *baz bim bam

", ""); + } + + // Rule 16: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example470() + { + // Example 470 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **foo **bar baz** + // + // Should be rendered as: + //

**foo bar baz

+ + Console.WriteLine("Example 470\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**foo **bar baz**", "

**foo bar baz

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example471() + { + // Example 471 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *foo *bar baz* + // + // Should be rendered as: + //

*foo bar baz

+ + Console.WriteLine("Example 471\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*foo *bar baz*", "

*foo bar baz

", ""); + } + + // Rule 17: + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example472() + { + // Example 472 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *[bar*](/url) + // + // Should be rendered as: + //

*bar*

+ + Console.WriteLine("Example 472\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*[bar*](/url)", "

*bar*

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example473() + { + // Example 473 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _foo [bar_](/url) + // + // Should be rendered as: + //

_foo bar_

+ + Console.WriteLine("Example 473\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_foo [bar_](/url)", "

_foo bar_

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example474() + { + // Example 474 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // * + // + // Should be rendered as: + //

*

+ + Console.WriteLine("Example 474\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*", "

*

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example475() + { + // Example 475 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // ** + // + // Should be rendered as: + //

**

+ + Console.WriteLine("Example 475\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**", "

**

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example476() + { + // Example 476 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __ + // + // Should be rendered as: + //

__

+ + Console.WriteLine("Example 476\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__", "

__

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example477() + { + // Example 477 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // *a `*`* + // + // Should be rendered as: + //

a *

+ + Console.WriteLine("Example 477\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("*a `*`*", "

a *

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example478() + { + // Example 478 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // _a `_`_ + // + // Should be rendered as: + //

a _

+ + Console.WriteLine("Example 478\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("_a `_`_", "

a _

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example479() + { + // Example 479 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // **a + // + // Should be rendered as: + //

**ahttp://foo.bar/?q=**

+ + Console.WriteLine("Example 479\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("**a", "

**ahttp://foo.bar/?q=**

", ""); + } + + [Test] + public void InlinesEmphasisAndStrongEmphasis_Example480() + { + // Example 480 + // Section: Inlines / Emphasis and strong emphasis + // + // The following Markdown: + // __a + // + // Should be rendered as: + //

__ahttp://foo.bar/?q=__

+ + Console.WriteLine("Example 480\nSection Inlines / Emphasis and strong emphasis\n"); + TestRoundtrip.TestSpec("__a", "

__ahttp://foo.bar/?q=__

", ""); + } + } + + [TestFixture] + public class TestInlinesLinks + { + // ## Links + // + // A link contains [link text] (the visible text), a [link destination] + // (the URI that is the link destination), and optionally a [link title]. + // There are two basic kinds of links in Markdown. In [inline links] the + // destination and title are given immediately after the link text. In + // [reference links] the destination and title are defined elsewhere in + // the document. + // + // A [link text](@) consists of a sequence of zero or more + // inline elements enclosed by square brackets (`[` and `]`). The + // following rules apply: + // + // - Links may not contain other links, at any level of nesting. If + // multiple otherwise valid link definitions appear nested inside each + // other, the inner-most definition is used. + // + // - Brackets are allowed in the [link text] only if (a) they + // are backslash-escaped or (b) they appear as a matched pair of brackets, + // with an open bracket `[`, a sequence of zero or more inlines, and + // a close bracket `]`. + // + // - Backtick [code spans], [autolinks], and raw [HTML tags] bind more tightly + // than the brackets in link text. Thus, for example, + // `` [foo`]` `` could not be a link text, since the second `]` + // is part of a code span. + // + // - The brackets in link text bind more tightly than markers for + // [emphasis and strong emphasis]. Thus, for example, `*[foo*](url)` is a link. + // + // A [link destination](@) consists of either + // + // - a sequence of zero or more characters between an opening `<` and a + // closing `>` that contains no line breaks or unescaped + // `<` or `>` characters, or + // + // - a nonempty sequence of characters that does not start with + // `<`, does not include ASCII space or control characters, and + // includes parentheses only if (a) they are backslash-escaped or + // (b) they are part of a balanced pair of unescaped parentheses. + // (Implementations may impose limits on parentheses nesting to + // avoid performance issues, but at least three levels of nesting + // should be supported.) + // + // A [link title](@) consists of either + // + // - a sequence of zero or more characters between straight double-quote + // characters (`"`), including a `"` character only if it is + // backslash-escaped, or + // + // - a sequence of zero or more characters between straight single-quote + // characters (`'`), including a `'` character only if it is + // backslash-escaped, or + // + // - a sequence of zero or more characters between matching parentheses + // (`(...)`), including a `(` or `)` character only if it is + // backslash-escaped. + // + // Although [link titles] may span multiple lines, they may not contain + // a [blank line]. + // + // An [inline link](@) consists of a [link text] followed immediately + // by a left parenthesis `(`, optional [whitespace], an optional + // [link destination], an optional [link title] separated from the link + // destination by [whitespace], optional [whitespace], and a right + // parenthesis `)`. The link's text consists of the inlines contained + // in the [link text] (excluding the enclosing square brackets). + // The link's URI consists of the link destination, excluding enclosing + // `<...>` if present, with backslash-escapes in effect as described + // above. The link's title consists of the link title, excluding its + // enclosing delimiters, with backslash-escapes in effect as described + // above. + // + // Here is a simple inline link: + [Test] + public void InlinesLinks_Example481() + { + // Example 481 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/uri "title") + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 481\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/uri \"title\")", "

link

", ""); + } + + // The title may be omitted: + [Test] + public void InlinesLinks_Example482() + { + // Example 482 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/uri) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 482\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/uri)", "

link

", ""); + } + + // Both the title and the destination may be omitted: + [Test] + public void InlinesLinks_Example483() + { + // Example 483 + // Section: Inlines / Links + // + // The following Markdown: + // [link]() + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 483\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link]()", "

link

", ""); + } + + [Test] + public void InlinesLinks_Example484() + { + // Example 484 + // Section: Inlines / Links + // + // The following Markdown: + // [link](<>) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 484\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](<>)", "

link

", ""); + } + + // The destination can only contain spaces if it is + // enclosed in pointy brackets: + [Test] + public void InlinesLinks_Example485() + { + // Example 485 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/my uri) + // + // Should be rendered as: + //

[link](/my uri)

+ + Console.WriteLine("Example 485\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/my uri)", "

[link](/my uri)

", ""); + } + + [Test] + public void InlinesLinks_Example486() + { + // Example 486 + // Section: Inlines / Links + // + // The following Markdown: + // [link](
) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 486\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](
)", "

link

", ""); + } + + // The destination cannot contain line breaks, + // even if enclosed in pointy brackets: + [Test] + public void InlinesLinks_Example487() + { + // Example 487 + // Section: Inlines / Links + // + // The following Markdown: + // [link](foo + // bar) + // + // Should be rendered as: + //

[link](foo + // bar)

+ + Console.WriteLine("Example 487\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](foo\nbar)", "

[link](foo\nbar)

", ""); + } + + [Test] + public void InlinesLinks_Example488() + { + // Example 488 + // Section: Inlines / Links + // + // The following Markdown: + // [link]() + // + // Should be rendered as: + //

[link]()

+ + Console.WriteLine("Example 488\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link]()", "

[link]()

", ""); + } + + // The destination can contain `)` if it is enclosed + // in pointy brackets: + [Test] + public void InlinesLinks_Example489() + { + // Example 489 + // Section: Inlines / Links + // + // The following Markdown: + // [a]() + // + // Should be rendered as: + //

a

+ + Console.WriteLine("Example 489\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[a]()", "

a

", ""); + } + + // Pointy brackets that enclose links must be unescaped: + [Test] + public void InlinesLinks_Example490() + { + // Example 490 + // Section: Inlines / Links + // + // The following Markdown: + // [link]() + // + // Should be rendered as: + //

[link](<foo>)

+ + Console.WriteLine("Example 490\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link]()", "

[link](<foo>)

", ""); + } + + // These are not links, because the opening pointy bracket + // is not matched properly: + [Test] + public void InlinesLinks_Example491() + { + // Example 491 + // Section: Inlines / Links + // + // The following Markdown: + // [a]( + // [a](c) + // + // Should be rendered as: + //

[a](<b)c + // [a](<b)c> + // [a](c)

+ + Console.WriteLine("Example 491\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[a](\n[a](c)", "

[a](<b)c\n[a](<b)c>\n[a](c)

", ""); + } + + // Parentheses inside the link destination may be escaped: + [Test] + public void InlinesLinks_Example492() + { + // Example 492 + // Section: Inlines / Links + // + // The following Markdown: + // [link](\(foo\)) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 492\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](\\(foo\\))", "

link

", ""); + } + + // Any number of parentheses are allowed without escaping, as long as they are + // balanced: + [Test] + public void InlinesLinks_Example493() + { + // Example 493 + // Section: Inlines / Links + // + // The following Markdown: + // [link](foo(and(bar))) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 493\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](foo(and(bar)))", "

link

", ""); + } + + // However, if you have unbalanced parentheses, you need to escape or use the + // `<...>` form: + [Test] + public void InlinesLinks_Example494() + { + // Example 494 + // Section: Inlines / Links + // + // The following Markdown: + // [link](foo\(and\(bar\)) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 494\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](foo\\(and\\(bar\\))", "

link

", ""); + } + + [Test] + public void InlinesLinks_Example495() + { + // Example 495 + // Section: Inlines / Links + // + // The following Markdown: + // [link]() + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 495\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link]()", "

link

", ""); + } + + // Parentheses and other symbols can also be escaped, as usual + // in Markdown: + [Test] + public void InlinesLinks_Example496() + { + // Example 496 + // Section: Inlines / Links + // + // The following Markdown: + // [link](foo\)\:) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 496\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](foo\\)\\:)", "

link

", ""); + } + + // A link can contain fragment identifiers and queries: + [Test] + public void InlinesLinks_Example497() + { + // Example 497 + // Section: Inlines / Links + // + // The following Markdown: + // [link](#fragment) + // + // [link](http://example.com#fragment) + // + // [link](http://example.com?foo=3#frag) + // + // Should be rendered as: + //

link

+ //

link

+ //

link

+ + Console.WriteLine("Example 497\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)", "

link

\n

link

\n

link

", ""); + } + + // Note that a backslash before a non-escapable character is + // just a backslash: + [Test] + public void InlinesLinks_Example498() + { + // Example 498 + // Section: Inlines / Links + // + // The following Markdown: + // [link](foo\bar) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 498\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](foo\\bar)", "

link

", ""); + } + + // URL-escaping should be left alone inside the destination, as all + // URL-escaped characters are also valid URL characters. Entity and + // numerical character references in the destination will be parsed + // into the corresponding Unicode code points, as usual. These may + // be optionally URL-escaped when written as HTML, but this spec + // does not enforce any particular policy for rendering URLs in + // HTML or other formats. Renderers may make different decisions + // about how to escape or normalize URLs in the output. + [Test] + public void InlinesLinks_Example499() + { + // Example 499 + // Section: Inlines / Links + // + // The following Markdown: + // [link](foo%20bä) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 499\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](foo%20bä)", "

link

", ""); + } + + // Note that, because titles can often be parsed as destinations, + // if you try to omit the destination and keep the title, you'll + // get unexpected results: + [Test] + public void InlinesLinks_Example500() + { + // Example 500 + // Section: Inlines / Links + // + // The following Markdown: + // [link]("title") + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 500\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](\"title\")", "

link

", ""); + } + + // Titles may be in single quotes, double quotes, or parentheses: + [Test] + public void InlinesLinks_Example501() + { + // Example 501 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/url "title") + // [link](/url 'title') + // [link](/url (title)) + // + // Should be rendered as: + //

link + // link + // link

+ + Console.WriteLine("Example 501\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))", "

link\nlink\nlink

", ""); + } + + // Backslash escapes and entity and numeric character references + // may be used in titles: + [Test] + public void InlinesLinks_Example502() + { + // Example 502 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/url "title \""") + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 502\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/url \"title \\\""\")", "

link

", ""); + } + + // Titles must be separated from the link using a [whitespace]. + // Other [Unicode whitespace] like non-breaking space doesn't work. + [Test] + public void InlinesLinks_Example503() + { + // Example 503 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/url "title") + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 503\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/url \"title\")", "

link

", ""); + } + + // Nested balanced quotes are not allowed without escaping: + [Test] + public void InlinesLinks_Example504() + { + // Example 504 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/url "title "and" title") + // + // Should be rendered as: + //

[link](/url "title "and" title")

+ + Console.WriteLine("Example 504\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/url \"title \"and\" title\")", "

[link](/url "title "and" title")

", ""); + } + + // But it is easy to work around this by using a different quote type: + [Test] + public void InlinesLinks_Example505() + { + // Example 505 + // Section: Inlines / Links + // + // The following Markdown: + // [link](/url 'title "and" title') + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 505\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link](/url 'title \"and\" title')", "

link

", ""); + } + + // (Note: `Markdown.pl` did allow double quotes inside a double-quoted + // title, and its test suite included a test demonstrating this. + // But it is hard to see a good rationale for the extra complexity this + // brings, since there are already many ways---backslash escaping, + // entity and numeric character references, or using a different + // quote type for the enclosing title---to write titles containing + // double quotes. `Markdown.pl`'s handling of titles has a number + // of other strange features. For example, it allows single-quoted + // titles in inline links, but not reference links. And, in + // reference links but not inline links, it allows a title to begin + // with `"` and end with `)`. `Markdown.pl` 1.0.1 even allows + // titles with no closing quotation mark, though 1.0.2b8 does not. + // It seems preferable to adopt a simple, rational rule that works + // the same way in inline links and link reference definitions.) + // + // [Whitespace] is allowed around the destination and title: + [Test] + public void InlinesLinks_Example506() + { + // Example 506 + // Section: Inlines / Links + // + // The following Markdown: + // [link]( /uri + // "title" ) + // + // Should be rendered as: + //

link

+ + Console.WriteLine("Example 506\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link]( /uri\n \"title\" )", "

link

", ""); + } + + // But it is not allowed between the link text and the + // following parenthesis: + [Test] + public void InlinesLinks_Example507() + { + // Example 507 + // Section: Inlines / Links + // + // The following Markdown: + // [link] (/uri) + // + // Should be rendered as: + //

[link] (/uri)

+ + Console.WriteLine("Example 507\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link] (/uri)", "

[link] (/uri)

", ""); + } + + // The link text may contain balanced brackets, but not unbalanced ones, + // unless they are escaped: + [Test] + public void InlinesLinks_Example508() + { + // Example 508 + // Section: Inlines / Links + // + // The following Markdown: + // [link [foo [bar]]](/uri) + // + // Should be rendered as: + //

link [foo [bar]]

+ + Console.WriteLine("Example 508\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link [foo [bar]]](/uri)", "

link [foo [bar]]

", ""); + } + + [Test] + public void InlinesLinks_Example509() + { + // Example 509 + // Section: Inlines / Links + // + // The following Markdown: + // [link] bar](/uri) + // + // Should be rendered as: + //

[link] bar](/uri)

+ + Console.WriteLine("Example 509\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link] bar](/uri)", "

[link] bar](/uri)

", ""); + } + + [Test] + public void InlinesLinks_Example510() + { + // Example 510 + // Section: Inlines / Links + // + // The following Markdown: + // [link [bar](/uri) + // + // Should be rendered as: + //

[link bar

+ + Console.WriteLine("Example 510\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link [bar](/uri)", "

[link bar

", ""); + } + + [Test] + public void InlinesLinks_Example511() + { + // Example 511 + // Section: Inlines / Links + // + // The following Markdown: + // [link \[bar](/uri) + // + // Should be rendered as: + //

link [bar

+ + Console.WriteLine("Example 511\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link \\[bar](/uri)", "

link [bar

", ""); + } + + // The link text may contain inline content: + [Test] + public void InlinesLinks_Example512() + { + // Example 512 + // Section: Inlines / Links + // + // The following Markdown: + // [link *foo **bar** `#`*](/uri) + // + // Should be rendered as: + //

link foo bar #

+ + Console.WriteLine("Example 512\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link *foo **bar** `#`*](/uri)", "

link foo bar #

", ""); + } + + [Test] + public void InlinesLinks_Example513() + { + // Example 513 + // Section: Inlines / Links + // + // The following Markdown: + // [![moon](moon.jpg)](/uri) + // + // Should be rendered as: + //

moon

+ + Console.WriteLine("Example 513\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[![moon](moon.jpg)](/uri)", "

\"moon\"

", ""); + } + + // However, links may not contain other links, at any level of nesting. + [Test] + public void InlinesLinks_Example514() + { + // Example 514 + // Section: Inlines / Links + // + // The following Markdown: + // [foo [bar](/uri)](/uri) + // + // Should be rendered as: + //

[foo bar](/uri)

+ + Console.WriteLine("Example 514\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo [bar](/uri)](/uri)", "

[foo bar](/uri)

", ""); + } + + [Test] + public void InlinesLinks_Example515() + { + // Example 515 + // Section: Inlines / Links + // + // The following Markdown: + // [foo *[bar [baz](/uri)](/uri)*](/uri) + // + // Should be rendered as: + //

[foo [bar baz](/uri)](/uri)

+ + Console.WriteLine("Example 515\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo *[bar [baz](/uri)](/uri)*](/uri)", "

[foo [bar baz](/uri)](/uri)

", ""); + } + + [Test] + public void InlinesLinks_Example516() + { + // Example 516 + // Section: Inlines / Links + // + // The following Markdown: + // ![[[foo](uri1)](uri2)](uri3) + // + // Should be rendered as: + //

[foo](uri2)

+ + Console.WriteLine("Example 516\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("![[[foo](uri1)](uri2)](uri3)", "

\"[foo](uri2)\"

", ""); + } + + // These cases illustrate the precedence of link text grouping over + // emphasis grouping: + [Test] + public void InlinesLinks_Example517() + { + // Example 517 + // Section: Inlines / Links + // + // The following Markdown: + // *[foo*](/uri) + // + // Should be rendered as: + //

*foo*

+ + Console.WriteLine("Example 517\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("*[foo*](/uri)", "

*foo*

", ""); + } + + [Test] + public void InlinesLinks_Example518() + { + // Example 518 + // Section: Inlines / Links + // + // The following Markdown: + // [foo *bar](baz*) + // + // Should be rendered as: + //

foo *bar

+ + Console.WriteLine("Example 518\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo *bar](baz*)", "

foo *bar

", ""); + } + + // Note that brackets that *aren't* part of links do not take + // precedence: + [Test] + public void InlinesLinks_Example519() + { + // Example 519 + // Section: Inlines / Links + // + // The following Markdown: + // *foo [bar* baz] + // + // Should be rendered as: + //

foo [bar baz]

+ + Console.WriteLine("Example 519\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("*foo [bar* baz]", "

foo [bar baz]

", ""); + } + + // These cases illustrate the precedence of HTML tags, code spans, + // and autolinks over link grouping: + [Test] + public void InlinesLinks_Example520() + { + // Example 520 + // Section: Inlines / Links + // + // The following Markdown: + // [foo + // + // Should be rendered as: + //

[foo

+ + Console.WriteLine("Example 520\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo ", "

[foo

", ""); + } + + [Test] + public void InlinesLinks_Example521() + { + // Example 521 + // Section: Inlines / Links + // + // The following Markdown: + // [foo`](/uri)` + // + // Should be rendered as: + //

[foo](/uri)

+ + Console.WriteLine("Example 521\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo`](/uri)`", "

[foo](/uri)

", ""); + } + + [Test] + public void InlinesLinks_Example522() + { + // Example 522 + // Section: Inlines / Links + // + // The following Markdown: + // [foo + // + // Should be rendered as: + //

[foohttp://example.com/?search=](uri)

+ + Console.WriteLine("Example 522\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo", "

[foohttp://example.com/?search=](uri)

", ""); + } + + // There are three kinds of [reference link](@)s: + // [full](#full-reference-link), [collapsed](#collapsed-reference-link), + // and [shortcut](#shortcut-reference-link). + // + // A [full reference link](@) + // consists of a [link text] immediately followed by a [link label] + // that [matches] a [link reference definition] elsewhere in the document. + // + // A [link label](@) begins with a left bracket (`[`) and ends + // with the first right bracket (`]`) that is not backslash-escaped. + // Between these brackets there must be at least one [non-whitespace character]. + // Unescaped square bracket characters are not allowed inside the + // opening and closing square brackets of [link labels]. A link + // label can have at most 999 characters inside the square + // brackets. + // + // One label [matches](@) + // another just in case their normalized forms are equal. To normalize a + // label, strip off the opening and closing brackets, + // perform the *Unicode case fold*, strip leading and trailing + // [whitespace] and collapse consecutive internal + // [whitespace] to a single space. If there are multiple + // matching reference link definitions, the one that comes first in the + // document is used. (It is desirable in such cases to emit a warning.) + // + // The contents of the first link label are parsed as inlines, which are + // used as the link's text. The link's URI and title are provided by the + // matching [link reference definition]. + // + // Here is a simple example: + [Test] + public void InlinesLinks_Example523() + { + // Example 523 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][bar] + // + // [bar]: /url "title" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 523\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][bar]\n\n[bar]: /url \"title\"", "

foo

", ""); + } + + // The rules for the [link text] are the same as with + // [inline links]. Thus: + // + // The link text may contain balanced brackets, but not unbalanced ones, + // unless they are escaped: + [Test] + public void InlinesLinks_Example524() + { + // Example 524 + // Section: Inlines / Links + // + // The following Markdown: + // [link [foo [bar]]][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

link [foo [bar]]

+ + Console.WriteLine("Example 524\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link [foo [bar]]][ref]\n\n[ref]: /uri", "

link [foo [bar]]

", ""); + } + + [Test] + public void InlinesLinks_Example525() + { + // Example 525 + // Section: Inlines / Links + // + // The following Markdown: + // [link \[bar][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

link [bar

+ + Console.WriteLine("Example 525\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link \\[bar][ref]\n\n[ref]: /uri", "

link [bar

", ""); + } + + // The link text may contain inline content: + [Test] + public void InlinesLinks_Example526() + { + // Example 526 + // Section: Inlines / Links + // + // The following Markdown: + // [link *foo **bar** `#`*][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

link foo bar #

+ + Console.WriteLine("Example 526\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[link *foo **bar** `#`*][ref]\n\n[ref]: /uri", "

link foo bar #

", ""); + } + + [Test] + public void InlinesLinks_Example527() + { + // Example 527 + // Section: Inlines / Links + // + // The following Markdown: + // [![moon](moon.jpg)][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

moon

+ + Console.WriteLine("Example 527\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[![moon](moon.jpg)][ref]\n\n[ref]: /uri", "

\"moon\"

", ""); + } + + // However, links may not contain other links, at any level of nesting. + [Test] + public void InlinesLinks_Example528() + { + // Example 528 + // Section: Inlines / Links + // + // The following Markdown: + // [foo [bar](/uri)][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

[foo bar]ref

+ + Console.WriteLine("Example 528\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo [bar](/uri)][ref]\n\n[ref]: /uri", "

[foo bar]ref

", ""); + } + + [Test] + public void InlinesLinks_Example529() + { + // Example 529 + // Section: Inlines / Links + // + // The following Markdown: + // [foo *bar [baz][ref]*][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

[foo bar baz]ref

+ + Console.WriteLine("Example 529\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri", "

[foo bar baz]ref

", ""); + } + + // (In the examples above, we have two [shortcut reference links] + // instead of one [full reference link].) + // + // The following cases illustrate the precedence of link text grouping over + // emphasis grouping: + [Test] + public void InlinesLinks_Example530() + { + // Example 530 + // Section: Inlines / Links + // + // The following Markdown: + // *[foo*][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

*foo*

+ + Console.WriteLine("Example 530\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("*[foo*][ref]\n\n[ref]: /uri", "

*foo*

", ""); + } + + [Test] + public void InlinesLinks_Example531() + { + // Example 531 + // Section: Inlines / Links + // + // The following Markdown: + // [foo *bar][ref] + // + // [ref]: /uri + // + // Should be rendered as: + //

foo *bar

+ + Console.WriteLine("Example 531\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo *bar][ref]\n\n[ref]: /uri", "

foo *bar

", ""); + } + + // These cases illustrate the precedence of HTML tags, code spans, + // and autolinks over link grouping: + [Test] + public void InlinesLinks_Example532() + { + // Example 532 + // Section: Inlines / Links + // + // The following Markdown: + // [foo + // + // [ref]: /uri + // + // Should be rendered as: + //

[foo

+ + Console.WriteLine("Example 532\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo \n\n[ref]: /uri", "

[foo

", ""); + } + + [Test] + public void InlinesLinks_Example533() + { + // Example 533 + // Section: Inlines / Links + // + // The following Markdown: + // [foo`][ref]` + // + // [ref]: /uri + // + // Should be rendered as: + //

[foo][ref]

+ + Console.WriteLine("Example 533\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo`][ref]`\n\n[ref]: /uri", "

[foo][ref]

", ""); + } + + [Test] + public void InlinesLinks_Example534() + { + // Example 534 + // Section: Inlines / Links + // + // The following Markdown: + // [foo + // + // [ref]: /uri + // + // Should be rendered as: + //

[foohttp://example.com/?search=][ref]

+ + Console.WriteLine("Example 534\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo\n\n[ref]: /uri", "

[foohttp://example.com/?search=][ref]

", ""); + } + + // Matching is case-insensitive: + [Test] + public void InlinesLinks_Example535() + { + // Example 535 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][BaR] + // + // [bar]: /url "title" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 535\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][BaR]\n\n[bar]: /url \"title\"", "

foo

", ""); + } + + // Unicode case fold is used: + [Test] + public void InlinesLinks_Example536() + { + // Example 536 + // Section: Inlines / Links + // + // The following Markdown: + // [Толпой][Толпой] is a Russian word. + // + // [ТОЛПОЙ]: /url + // + // Should be rendered as: + //

Толпой is a Russian word.

+ + Console.WriteLine("Example 536\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url", "

Толпой is a Russian word.

", ""); + } + + // Consecutive internal [whitespace] is treated as one space for + // purposes of determining matching: + [Test] + public void InlinesLinks_Example537() + { + // Example 537 + // Section: Inlines / Links + // + // The following Markdown: + // [Foo + // bar]: /url + // + // [Baz][Foo bar] + // + // Should be rendered as: + //

Baz

+ + Console.WriteLine("Example 537\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[Foo\n bar]: /url\n\n[Baz][Foo bar]", "

Baz

", ""); + } + + // No [whitespace] is allowed between the [link text] and the + // [link label]: + [Test] + public void InlinesLinks_Example538() + { + // Example 538 + // Section: Inlines / Links + // + // The following Markdown: + // [foo] [bar] + // + // [bar]: /url "title" + // + // Should be rendered as: + //

[foo] bar

+ + Console.WriteLine("Example 538\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo] [bar]\n\n[bar]: /url \"title\"", "

[foo] bar

", ""); + } + + [Test] + public void InlinesLinks_Example539() + { + // Example 539 + // Section: Inlines / Links + // + // The following Markdown: + // [foo] + // [bar] + // + // [bar]: /url "title" + // + // Should be rendered as: + //

[foo] + // bar

+ + Console.WriteLine("Example 539\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo]\n[bar]\n\n[bar]: /url \"title\"", "

[foo]\nbar

", ""); + } + + // This is a departure from John Gruber's original Markdown syntax + // description, which explicitly allows whitespace between the link + // text and the link label. It brings reference links in line with + // [inline links], which (according to both original Markdown and + // this spec) cannot have whitespace after the link text. More + // importantly, it prevents inadvertent capture of consecutive + // [shortcut reference links]. If whitespace is allowed between the + // link text and the link label, then in the following we will have + // a single reference link, not two shortcut reference links, as + // intended: + // + // ``` markdown + // [foo] + // [bar] + // + // [foo]: /url1 + // [bar]: /url2 + // ``` + // + // (Note that [shortcut reference links] were introduced by Gruber + // himself in a beta version of `Markdown.pl`, but never included + // in the official syntax description. Without shortcut reference + // links, it is harmless to allow space between the link text and + // link label; but once shortcut references are introduced, it is + // too dangerous to allow this, as it frequently leads to + // unintended results.) + // + // When there are multiple matching [link reference definitions], + // the first is used: + [Test] + public void InlinesLinks_Example540() + { + // Example 540 + // Section: Inlines / Links + // + // The following Markdown: + // [foo]: /url1 + // + // [foo]: /url2 + // + // [bar][foo] + // + // Should be rendered as: + //

bar

+ + Console.WriteLine("Example 540\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]", "

bar

", ""); + } + + // Note that matching is performed on normalized strings, not parsed + // inline content. So the following does not match, even though the + // labels define equivalent inline content: + [Test] + public void InlinesLinks_Example541() + { + // Example 541 + // Section: Inlines / Links + // + // The following Markdown: + // [bar][foo\!] + // + // [foo!]: /url + // + // Should be rendered as: + //

[bar][foo!]

+ + Console.WriteLine("Example 541\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[bar][foo\\!]\n\n[foo!]: /url", "

[bar][foo!]

", ""); + } + + // [Link labels] cannot contain brackets, unless they are + // backslash-escaped: + [Test] + public void InlinesLinks_Example542() + { + // Example 542 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][ref[] + // + // [ref[]: /uri + // + // Should be rendered as: + //

[foo][ref[]

+ //

[ref[]: /uri

+ + Console.WriteLine("Example 542\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][ref[]\n\n[ref[]: /uri", "

[foo][ref[]

\n

[ref[]: /uri

", ""); + } + + [Test] + public void InlinesLinks_Example543() + { + // Example 543 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][ref[bar]] + // + // [ref[bar]]: /uri + // + // Should be rendered as: + //

[foo][ref[bar]]

+ //

[ref[bar]]: /uri

+ + Console.WriteLine("Example 543\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][ref[bar]]\n\n[ref[bar]]: /uri", "

[foo][ref[bar]]

\n

[ref[bar]]: /uri

", ""); + } + + [Test] + public void InlinesLinks_Example544() + { + // Example 544 + // Section: Inlines / Links + // + // The following Markdown: + // [[[foo]]] + // + // [[[foo]]]: /url + // + // Should be rendered as: + //

[[[foo]]]

+ //

[[[foo]]]: /url

+ + Console.WriteLine("Example 544\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[[[foo]]]\n\n[[[foo]]]: /url", "

[[[foo]]]

\n

[[[foo]]]: /url

", ""); + } + + [Test] + public void InlinesLinks_Example545() + { + // Example 545 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][ref\[] + // + // [ref\[]: /uri + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 545\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][ref\\[]\n\n[ref\\[]: /uri", "

foo

", ""); + } + + // Note that in this example `]` is not backslash-escaped: + [Test] + public void InlinesLinks_Example546() + { + // Example 546 + // Section: Inlines / Links + // + // The following Markdown: + // [bar\\]: /uri + // + // [bar\\] + // + // Should be rendered as: + //

bar\

+ + Console.WriteLine("Example 546\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[bar\\\\]: /uri\n\n[bar\\\\]", "

bar\\

", ""); + } + + // A [link label] must contain at least one [non-whitespace character]: + [Test] + public void InlinesLinks_Example547() + { + // Example 547 + // Section: Inlines / Links + // + // The following Markdown: + // [] + // + // []: /uri + // + // Should be rendered as: + //

[]

+ //

[]: /uri

+ + Console.WriteLine("Example 547\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[]\n\n[]: /uri", "

[]

\n

[]: /uri

", ""); + } + + [Test] + public void InlinesLinks_Example548() + { + // Example 548 + // Section: Inlines / Links + // + // The following Markdown: + // [ + // ] + // + // [ + // ]: /uri + // + // Should be rendered as: + //

[ + // ]

+ //

[ + // ]: /uri

+ + Console.WriteLine("Example 548\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[\n ]\n\n[\n ]: /uri", "

[\n]

\n

[\n]: /uri

", ""); + } + + // A [collapsed reference link](@) + // consists of a [link label] that [matches] a + // [link reference definition] elsewhere in the + // document, followed by the string `[]`. + // The contents of the first link label are parsed as inlines, + // which are used as the link's text. The link's URI and title are + // provided by the matching reference link definition. Thus, + // `[foo][]` is equivalent to `[foo][foo]`. + [Test] + public void InlinesLinks_Example549() + { + // Example 549 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 549\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][]\n\n[foo]: /url \"title\"", "

foo

", ""); + } + + [Test] + public void InlinesLinks_Example550() + { + // Example 550 + // Section: Inlines / Links + // + // The following Markdown: + // [*foo* bar][] + // + // [*foo* bar]: /url "title" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 550\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[*foo* bar][]\n\n[*foo* bar]: /url \"title\"", "

foo bar

", ""); + } + + // The link labels are case-insensitive: + [Test] + public void InlinesLinks_Example551() + { + // Example 551 + // Section: Inlines / Links + // + // The following Markdown: + // [Foo][] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 551\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[Foo][]\n\n[foo]: /url \"title\"", "

Foo

", ""); + } + + // As with full reference links, [whitespace] is not + // allowed between the two sets of brackets: + [Test] + public void InlinesLinks_Example552() + { + // Example 552 + // Section: Inlines / Links + // + // The following Markdown: + // [foo] + // [] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

foo + // []

+ + Console.WriteLine("Example 552\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo] \n[]\n\n[foo]: /url \"title\"", "

foo\n[]

", ""); + } + + // A [shortcut reference link](@) + // consists of a [link label] that [matches] a + // [link reference definition] elsewhere in the + // document and is not followed by `[]` or a link label. + // The contents of the first link label are parsed as inlines, + // which are used as the link's text. The link's URI and title + // are provided by the matching link reference definition. + // Thus, `[foo]` is equivalent to `[foo][]`. + [Test] + public void InlinesLinks_Example553() + { + // Example 553 + // Section: Inlines / Links + // + // The following Markdown: + // [foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 553\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo]\n\n[foo]: /url \"title\"", "

foo

", ""); + } + + [Test] + public void InlinesLinks_Example554() + { + // Example 554 + // Section: Inlines / Links + // + // The following Markdown: + // [*foo* bar] + // + // [*foo* bar]: /url "title" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 554\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[*foo* bar]\n\n[*foo* bar]: /url \"title\"", "

foo bar

", ""); + } + + [Test] + public void InlinesLinks_Example555() + { + // Example 555 + // Section: Inlines / Links + // + // The following Markdown: + // [[*foo* bar]] + // + // [*foo* bar]: /url "title" + // + // Should be rendered as: + //

[foo bar]

+ + Console.WriteLine("Example 555\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"", "

[foo bar]

", ""); + } + + [Test] + public void InlinesLinks_Example556() + { + // Example 556 + // Section: Inlines / Links + // + // The following Markdown: + // [[bar [foo] + // + // [foo]: /url + // + // Should be rendered as: + //

[[bar foo

+ + Console.WriteLine("Example 556\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[[bar [foo]\n\n[foo]: /url", "

[[bar foo

", ""); + } + + // The link labels are case-insensitive: + [Test] + public void InlinesLinks_Example557() + { + // Example 557 + // Section: Inlines / Links + // + // The following Markdown: + // [Foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 557\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[Foo]\n\n[foo]: /url \"title\"", "

Foo

", ""); + } + + // A space after the link text should be preserved: + [Test] + public void InlinesLinks_Example558() + { + // Example 558 + // Section: Inlines / Links + // + // The following Markdown: + // [foo] bar + // + // [foo]: /url + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 558\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo] bar\n\n[foo]: /url", "

foo bar

", ""); + } + + // If you just want bracketed text, you can backslash-escape the + // opening bracket to avoid links: + [Test] + public void InlinesLinks_Example559() + { + // Example 559 + // Section: Inlines / Links + // + // The following Markdown: + // \[foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

[foo]

+ + Console.WriteLine("Example 559\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("\\[foo]\n\n[foo]: /url \"title\"", "

[foo]

", ""); + } + + // Note that this is a link, because a link label ends with the first + // following closing bracket: + [Test] + public void InlinesLinks_Example560() + { + // Example 560 + // Section: Inlines / Links + // + // The following Markdown: + // [foo*]: /url + // + // *[foo*] + // + // Should be rendered as: + //

*foo*

+ + Console.WriteLine("Example 560\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo*]: /url\n\n*[foo*]", "

*foo*

", ""); + } + + // Full and compact references take precedence over shortcut + // references: + [Test] + public void InlinesLinks_Example561() + { + // Example 561 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][bar] + // + // [foo]: /url1 + // [bar]: /url2 + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 561\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][bar]\n\n[foo]: /url1\n[bar]: /url2", "

foo

", ""); + } + + [Test] + public void InlinesLinks_Example562() + { + // Example 562 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][] + // + // [foo]: /url1 + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 562\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][]\n\n[foo]: /url1", "

foo

", ""); + } + + // Inline links also take precedence: + [Test] + public void InlinesLinks_Example563() + { + // Example 563 + // Section: Inlines / Links + // + // The following Markdown: + // [foo]() + // + // [foo]: /url1 + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 563\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo]()\n\n[foo]: /url1", "

foo

", ""); + } + + [Test] + public void InlinesLinks_Example564() + { + // Example 564 + // Section: Inlines / Links + // + // The following Markdown: + // [foo](not a link) + // + // [foo]: /url1 + // + // Should be rendered as: + //

foo(not a link)

+ + Console.WriteLine("Example 564\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo](not a link)\n\n[foo]: /url1", "

foo(not a link)

", ""); + } + + // In the following case `[bar][baz]` is parsed as a reference, + // `[foo]` as normal text: + [Test] + public void InlinesLinks_Example565() + { + // Example 565 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][bar][baz] + // + // [baz]: /url + // + // Should be rendered as: + //

[foo]bar

+ + Console.WriteLine("Example 565\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][bar][baz]\n\n[baz]: /url", "

[foo]bar

", ""); + } + + // Here, though, `[foo][bar]` is parsed as a reference, since + // `[bar]` is defined: + [Test] + public void InlinesLinks_Example566() + { + // Example 566 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][bar][baz] + // + // [baz]: /url1 + // [bar]: /url2 + // + // Should be rendered as: + //

foobaz

+ + Console.WriteLine("Example 566\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2", "

foobaz

", ""); + } + + // Here `[foo]` is not parsed as a shortcut reference, because it + // is followed by a link label (even though `[bar]` is not defined): + [Test] + public void InlinesLinks_Example567() + { + // Example 567 + // Section: Inlines / Links + // + // The following Markdown: + // [foo][bar][baz] + // + // [baz]: /url1 + // [foo]: /url2 + // + // Should be rendered as: + //

[foo]bar

+ + Console.WriteLine("Example 567\nSection Inlines / Links\n"); + TestRoundtrip.TestSpec("[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2", "

[foo]bar

", ""); + } + } + + [TestFixture] + public class TestInlinesImages + { + // ## Images + // + // Syntax for images is like the syntax for links, with one + // difference. Instead of [link text], we have an + // [image description](@). The rules for this are the + // same as for [link text], except that (a) an + // image description starts with `![` rather than `[`, and + // (b) an image description may contain links. + // An image description has inline elements + // as its contents. When an image is rendered to HTML, + // this is standardly used as the image's `alt` attribute. + [Test] + public void InlinesImages_Example568() + { + // Example 568 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo](/url "title") + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 568\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo](/url \"title\")", "

\"foo\"

", ""); + } + + [Test] + public void InlinesImages_Example569() + { + // Example 569 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo *bar*] + // + // [foo *bar*]: train.jpg "train & tracks" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 569\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"", "

\"foo

", ""); + } + + [Test] + public void InlinesImages_Example570() + { + // Example 570 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo ![bar](/url)](/url2) + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 570\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo ![bar](/url)](/url2)", "

\"foo

", ""); + } + + [Test] + public void InlinesImages_Example571() + { + // Example 571 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo [bar](/url)](/url2) + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 571\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo [bar](/url)](/url2)", "

\"foo

", ""); + } + + // Though this spec is concerned with parsing, not rendering, it is + // recommended that in rendering to HTML, only the plain string content + // of the [image description] be used. Note that in + // the above example, the alt attribute's value is `foo bar`, not `foo + // [bar](/url)` or `foo bar`. Only the plain string + // content is rendered, without formatting. + [Test] + public void InlinesImages_Example572() + { + // Example 572 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo *bar*][] + // + // [foo *bar*]: train.jpg "train & tracks" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 572\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"", "

\"foo

", ""); + } + + [Test] + public void InlinesImages_Example573() + { + // Example 573 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo *bar*][foobar] + // + // [FOOBAR]: train.jpg "train & tracks" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 573\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"", "

\"foo

", ""); + } + + [Test] + public void InlinesImages_Example574() + { + // Example 574 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo](train.jpg) + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 574\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo](train.jpg)", "

\"foo\"

", ""); + } + + [Test] + public void InlinesImages_Example575() + { + // Example 575 + // Section: Inlines / Images + // + // The following Markdown: + // My ![foo bar](/path/to/train.jpg "title" ) + // + // Should be rendered as: + //

My foo bar

+ + Console.WriteLine("Example 575\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("My ![foo bar](/path/to/train.jpg \"title\" )", "

My \"foo

", ""); + } + + [Test] + public void InlinesImages_Example576() + { + // Example 576 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo]() + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 576\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo]()", "

\"foo\"

", ""); + } + + [Test] + public void InlinesImages_Example577() + { + // Example 577 + // Section: Inlines / Images + // + // The following Markdown: + // ![](/url) + // + // Should be rendered as: + //

+ + Console.WriteLine("Example 577\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![](/url)", "

\"\"

", ""); + } + + // Reference-style: + [Test] + public void InlinesImages_Example578() + { + // Example 578 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo][bar] + // + // [bar]: /url + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 578\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo][bar]\n\n[bar]: /url", "

\"foo\"

", ""); + } + + [Test] + public void InlinesImages_Example579() + { + // Example 579 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo][bar] + // + // [BAR]: /url + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 579\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo][bar]\n\n[BAR]: /url", "

\"foo\"

", ""); + } + + // Collapsed: + [Test] + public void InlinesImages_Example580() + { + // Example 580 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo][] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 580\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo][]\n\n[foo]: /url \"title\"", "

\"foo\"

", ""); + } + + [Test] + public void InlinesImages_Example581() + { + // Example 581 + // Section: Inlines / Images + // + // The following Markdown: + // ![*foo* bar][] + // + // [*foo* bar]: /url "title" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 581\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![*foo* bar][]\n\n[*foo* bar]: /url \"title\"", "

\"foo

", ""); + } + + // The labels are case-insensitive: + [Test] + public void InlinesImages_Example582() + { + // Example 582 + // Section: Inlines / Images + // + // The following Markdown: + // ![Foo][] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 582\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![Foo][]\n\n[foo]: /url \"title\"", "

\"Foo\"

", ""); + } + + // As with reference links, [whitespace] is not allowed + // between the two sets of brackets: + [Test] + public void InlinesImages_Example583() + { + // Example 583 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo] + // [] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

foo + // []

+ + Console.WriteLine("Example 583\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo] \n[]\n\n[foo]: /url \"title\"", "

\"foo\"\n[]

", ""); + } + + // Shortcut: + [Test] + public void InlinesImages_Example584() + { + // Example 584 + // Section: Inlines / Images + // + // The following Markdown: + // ![foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 584\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![foo]\n\n[foo]: /url \"title\"", "

\"foo\"

", ""); + } + + [Test] + public void InlinesImages_Example585() + { + // Example 585 + // Section: Inlines / Images + // + // The following Markdown: + // ![*foo* bar] + // + // [*foo* bar]: /url "title" + // + // Should be rendered as: + //

foo bar

+ + Console.WriteLine("Example 585\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![*foo* bar]\n\n[*foo* bar]: /url \"title\"", "

\"foo

", ""); + } + + // Note that link labels cannot contain unescaped brackets: + [Test] + public void InlinesImages_Example586() + { + // Example 586 + // Section: Inlines / Images + // + // The following Markdown: + // ![[foo]] + // + // [[foo]]: /url "title" + // + // Should be rendered as: + //

![[foo]]

+ //

[[foo]]: /url "title"

+ + Console.WriteLine("Example 586\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![[foo]]\n\n[[foo]]: /url \"title\"", "

![[foo]]

\n

[[foo]]: /url "title"

", ""); + } + + // The link labels are case-insensitive: + [Test] + public void InlinesImages_Example587() + { + // Example 587 + // Section: Inlines / Images + // + // The following Markdown: + // ![Foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 587\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("![Foo]\n\n[foo]: /url \"title\"", "

\"Foo\"

", ""); + } + + // If you just want a literal `!` followed by bracketed text, you can + // backslash-escape the opening `[`: + [Test] + public void InlinesImages_Example588() + { + // Example 588 + // Section: Inlines / Images + // + // The following Markdown: + // !\[foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

![foo]

+ + Console.WriteLine("Example 588\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("!\\[foo]\n\n[foo]: /url \"title\"", "

![foo]

", ""); + } + + // If you want a link after a literal `!`, backslash-escape the + // `!`: + [Test] + public void InlinesImages_Example589() + { + // Example 589 + // Section: Inlines / Images + // + // The following Markdown: + // \![foo] + // + // [foo]: /url "title" + // + // Should be rendered as: + //

!foo

+ + Console.WriteLine("Example 589\nSection Inlines / Images\n"); + TestRoundtrip.TestSpec("\\![foo]\n\n[foo]: /url \"title\"", "

!foo

", ""); + } + } + + [TestFixture] + public class TestInlinesAutolinks + { + // ## Autolinks + // + // [Autolink](@)s are absolute URIs and email addresses inside + // `<` and `>`. They are parsed as links, with the URL or email address + // as the link label. + // + // A [URI autolink](@) consists of `<`, followed by an + // [absolute URI] followed by `>`. It is parsed as + // a link to the URI, with the URI as the link's label. + // + // An [absolute URI](@), + // for these purposes, consists of a [scheme] followed by a colon (`:`) + // followed by zero or more characters other than ASCII + // [whitespace] and control characters, `<`, and `>`. If + // the URI includes these characters, they must be percent-encoded + // (e.g. `%20` for a space). + // + // For purposes of this spec, a [scheme](@) is any sequence + // of 2--32 characters beginning with an ASCII letter and followed + // by any combination of ASCII letters, digits, or the symbols plus + // ("+"), period ("."), or hyphen ("-"). + // + // Here are some valid autolinks: + [Test] + public void InlinesAutolinks_Example590() + { + // Example 590 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

http://foo.bar.baz

+ + Console.WriteLine("Example 590\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

http://foo.bar.baz

", ""); + } + + [Test] + public void InlinesAutolinks_Example591() + { + // Example 591 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

http://foo.bar.baz/test?q=hello&id=22&boolean

+ + Console.WriteLine("Example 591\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

http://foo.bar.baz/test?q=hello&id=22&boolean

", ""); + } + + [Test] + public void InlinesAutolinks_Example592() + { + // Example 592 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

irc://foo.bar:2233/baz

+ + Console.WriteLine("Example 592\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

irc://foo.bar:2233/baz

", ""); + } + + // Uppercase is also fine: + [Test] + public void InlinesAutolinks_Example593() + { + // Example 593 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

MAILTO:FOO@BAR.BAZ

+ + Console.WriteLine("Example 593\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

MAILTO:FOO@BAR.BAZ

", ""); + } + + // Note that many strings that count as [absolute URIs] for + // purposes of this spec are not valid URIs, because their + // schemes are not registered or because of other problems + // with their syntax: + [Test] + public void InlinesAutolinks_Example594() + { + // Example 594 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

a+b+c:d

+ + Console.WriteLine("Example 594\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

a+b+c:d

", ""); + } + + [Test] + public void InlinesAutolinks_Example595() + { + // Example 595 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

made-up-scheme://foo,bar

+ + Console.WriteLine("Example 595\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

made-up-scheme://foo,bar

", ""); + } + + [Test] + public void InlinesAutolinks_Example596() + { + // Example 596 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

http://../

+ + Console.WriteLine("Example 596\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

http://../

", ""); + } + + [Test] + public void InlinesAutolinks_Example597() + { + // Example 597 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

localhost:5001/foo

+ + Console.WriteLine("Example 597\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

localhost:5001/foo

", ""); + } + + // Spaces are not allowed in autolinks: + [Test] + public void InlinesAutolinks_Example598() + { + // Example 598 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

<http://foo.bar/baz bim>

+ + Console.WriteLine("Example 598\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

<http://foo.bar/baz bim>

", ""); + } + + // Backslash-escapes do not work inside autolinks: + [Test] + public void InlinesAutolinks_Example599() + { + // Example 599 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

http://example.com/\[\

+ + Console.WriteLine("Example 599\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

http://example.com/\\[\\

", ""); + } + + // An [email autolink](@) + // consists of `<`, followed by an [email address], + // followed by `>`. The link's label is the email address, + // and the URL is `mailto:` followed by the email address. + // + // An [email address](@), + // for these purposes, is anything that matches + // the [non-normative regex from the HTML5 + // spec](https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email)): + // + // /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? + // (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + // + // Examples of email autolinks: + [Test] + public void InlinesAutolinks_Example600() + { + // Example 600 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

foo@bar.example.com

+ + Console.WriteLine("Example 600\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

foo@bar.example.com

", ""); + } + + [Test] + public void InlinesAutolinks_Example601() + { + // Example 601 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

foo+special@Bar.baz-bar0.com

+ + Console.WriteLine("Example 601\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

foo+special@Bar.baz-bar0.com

", ""); + } + + // Backslash-escapes do not work inside email autolinks: + [Test] + public void InlinesAutolinks_Example602() + { + // Example 602 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

<foo+@bar.example.com>

+ + Console.WriteLine("Example 602\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

<foo+@bar.example.com>

", ""); + } + + // These are not autolinks: + [Test] + public void InlinesAutolinks_Example603() + { + // Example 603 + // Section: Inlines / Autolinks + // + // The following Markdown: + // <> + // + // Should be rendered as: + //

<>

+ + Console.WriteLine("Example 603\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("<>", "

<>

", ""); + } + + [Test] + public void InlinesAutolinks_Example604() + { + // Example 604 + // Section: Inlines / Autolinks + // + // The following Markdown: + // < http://foo.bar > + // + // Should be rendered as: + //

< http://foo.bar >

+ + Console.WriteLine("Example 604\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("< http://foo.bar >", "

< http://foo.bar >

", ""); + } + + [Test] + public void InlinesAutolinks_Example605() + { + // Example 605 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

<m:abc>

+ + Console.WriteLine("Example 605\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

<m:abc>

", ""); + } + + [Test] + public void InlinesAutolinks_Example606() + { + // Example 606 + // Section: Inlines / Autolinks + // + // The following Markdown: + // + // + // Should be rendered as: + //

<foo.bar.baz>

+ + Console.WriteLine("Example 606\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("", "

<foo.bar.baz>

", ""); + } + + [Test] + public void InlinesAutolinks_Example607() + { + // Example 607 + // Section: Inlines / Autolinks + // + // The following Markdown: + // http://example.com + // + // Should be rendered as: + //

http://example.com

+ + Console.WriteLine("Example 607\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("http://example.com", "

http://example.com

", ""); + } + + [Test] + public void InlinesAutolinks_Example608() + { + // Example 608 + // Section: Inlines / Autolinks + // + // The following Markdown: + // foo@bar.example.com + // + // Should be rendered as: + //

foo@bar.example.com

+ + Console.WriteLine("Example 608\nSection Inlines / Autolinks\n"); + TestRoundtrip.TestSpec("foo@bar.example.com", "

foo@bar.example.com

", ""); + } + } + + [TestFixture] + public class TestInlinesRawHTML + { + // ## Raw HTML + // + // Text between `<` and `>` that looks like an HTML tag is parsed as a + // raw HTML tag and will be rendered in HTML without escaping. + // Tag and attribute names are not limited to current HTML tags, + // so custom tags (and even, say, DocBook tags) may be used. + // + // Here is the grammar for tags: + // + // A [tag name](@) consists of an ASCII letter + // followed by zero or more ASCII letters, digits, or + // hyphens (`-`). + // + // An [attribute](@) consists of [whitespace], + // an [attribute name], and an optional + // [attribute value specification]. + // + // An [attribute name](@) + // consists of an ASCII letter, `_`, or `:`, followed by zero or more ASCII + // letters, digits, `_`, `.`, `:`, or `-`. (Note: This is the XML + // specification restricted to ASCII. HTML5 is laxer.) + // + // An [attribute value specification](@) + // consists of optional [whitespace], + // a `=` character, optional [whitespace], and an [attribute + // value]. + // + // An [attribute value](@) + // consists of an [unquoted attribute value], + // a [single-quoted attribute value], or a [double-quoted attribute value]. + // + // An [unquoted attribute value](@) + // is a nonempty string of characters not + // including [whitespace], `"`, `'`, `=`, `<`, `>`, or `` ` ``. + // + // A [single-quoted attribute value](@) + // consists of `'`, zero or more + // characters not including `'`, and a final `'`. + // + // A [double-quoted attribute value](@) + // consists of `"`, zero or more + // characters not including `"`, and a final `"`. + // + // An [open tag](@) consists of a `<` character, a [tag name], + // zero or more [attributes], optional [whitespace], an optional `/` + // character, and a `>` character. + // + // A [closing tag](@) consists of the string ``. + // + // An [HTML comment](@) consists of ``, + // where *text* does not start with `>` or `->`, does not end with `-`, + // and does not contain `--`. (See the + // [HTML5 spec](http://www.w3.org/TR/html5/syntax.html#comments).) + // + // A [processing instruction](@) + // consists of the string ``, and the string + // `?>`. + // + // A [declaration](@) consists of the + // string ``, and the character `>`. + // + // A [CDATA section](@) consists of + // the string ``, and the string `]]>`. + // + // An [HTML tag](@) consists of an [open tag], a [closing tag], + // an [HTML comment], a [processing instruction], a [declaration], + // or a [CDATA section]. + // + // Here are some simple open tags: + [Test] + public void InlinesRawHTML_Example609() + { + // Example 609 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // + // + // Should be rendered as: + //

+ + Console.WriteLine("Example 609\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + // Empty elements: + [Test] + public void InlinesRawHTML_Example610() + { + // Example 610 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // + // + // Should be rendered as: + //

+ + Console.WriteLine("Example 610\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + // [Whitespace] is allowed: + [Test] + public void InlinesRawHTML_Example611() + { + // Example 611 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // + // + // Should be rendered as: + //

+ + Console.WriteLine("Example 611\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + // With attributes: + [Test] + public void InlinesRawHTML_Example612() + { + // Example 612 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // + // + // Should be rendered as: + //

+ + Console.WriteLine("Example 612\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + // Custom tag names can be used: + [Test] + public void InlinesRawHTML_Example613() + { + // Example 613 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // Foo + // + // Should be rendered as: + //

Foo

+ + Console.WriteLine("Example 613\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("Foo ", "

Foo

", ""); + } + + // Illegal tag names, not parsed as HTML: + [Test] + public void InlinesRawHTML_Example614() + { + // Example 614 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // <33> <__> + // + // Should be rendered as: + //

<33> <__>

+ + Console.WriteLine("Example 614\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("<33> <__>", "

<33> <__>

", ""); + } + + // Illegal attribute names: + [Test] + public void InlinesRawHTML_Example615() + { + // Example 615 + // Section: Inlines / Raw HTML + // + // The following Markdown: + //
+ // + // Should be rendered as: + //

<a h*#ref="hi">

+ + Console.WriteLine("Example 615\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("
", "

<a h*#ref="hi">

", ""); + } + + // Illegal attribute values: + [Test] + public void InlinesRawHTML_Example616() + { + // Example 616 + // Section: Inlines / Raw HTML + // + // The following Markdown: + //
", "

<a href="hi'> <a href=hi'>

", ""); + } + + // Illegal [whitespace]: + [Test] + public void InlinesRawHTML_Example617() + { + // Example 617 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // < a>< + // foo> + // + // + // Should be rendered as: + //

< a>< + // foo><bar/ > + // <foo bar=baz + // bim!bop />

+ + Console.WriteLine("Example 617\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("< a><\nfoo>\n", "

< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />

", ""); + } + + // Missing [whitespace]: + [Test] + public void InlinesRawHTML_Example618() + { + // Example 618 + // Section: Inlines / Raw HTML + // + // The following Markdown: + //
+ // + // Should be rendered as: + //

<a href='bar'title=title>

+ + Console.WriteLine("Example 618\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("
", "

<a href='bar'title=title>

", ""); + } + + // Closing tags: + [Test] + public void InlinesRawHTML_Example619() + { + // Example 619 + // Section: Inlines / Raw HTML + // + // The following Markdown: + //
+ // + // Should be rendered as: + //

+ + Console.WriteLine("Example 619\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + // Illegal attributes in closing tag: + [Test] + public void InlinesRawHTML_Example620() + { + // Example 620 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // + // + // Should be rendered as: + //

</a href="foo">

+ + Console.WriteLine("Example 620\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("", "

</a href="foo">

", ""); + } + + // Comments: + [Test] + public void InlinesRawHTML_Example621() + { + // Example 621 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 621\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo ", "

foo

", ""); + } + + [Test] + public void InlinesRawHTML_Example622() + { + // Example 622 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo <!-- not a comment -- two hyphens -->

+ + Console.WriteLine("Example 622\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo ", "

foo <!-- not a comment -- two hyphens -->

", ""); + } + + // Not comments: + [Test] + public void InlinesRawHTML_Example623() + { + // Example 623 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo foo --> + // + // foo + // + // Should be rendered as: + //

foo <!--> foo -->

+ //

foo <!-- foo--->

+ + Console.WriteLine("Example 623\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo foo -->\n\nfoo ", "

foo <!--> foo -->

\n

foo <!-- foo--->

", ""); + } + + // Processing instructions: + [Test] + public void InlinesRawHTML_Example624() + { + // Example 624 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 624\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo ", "

foo

", ""); + } + + // Declarations: + [Test] + public void InlinesRawHTML_Example625() + { + // Example 625 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 625\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo ", "

foo

", ""); + } + + // CDATA sections: + [Test] + public void InlinesRawHTML_Example626() + { + // Example 626 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo &<]]> + // + // Should be rendered as: + //

foo &<]]>

+ + Console.WriteLine("Example 626\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo &<]]>", "

foo &<]]>

", ""); + } + + // Entity and numeric character references are preserved in HTML + // attributes: + [Test] + public void InlinesRawHTML_Example627() + { + // Example 627 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 627\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo ", "

foo

", ""); + } + + // Backslash escapes do not work in HTML attributes: + [Test] + public void InlinesRawHTML_Example628() + { + // Example 628 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 628\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("foo ", "

foo

", ""); + } + + [Test] + public void InlinesRawHTML_Example629() + { + // Example 629 + // Section: Inlines / Raw HTML + // + // The following Markdown: + // + // + // Should be rendered as: + //

<a href=""">

+ + Console.WriteLine("Example 629\nSection Inlines / Raw HTML\n"); + TestRoundtrip.TestSpec("
", "

<a href=""">

", ""); + } + } + + [TestFixture] + public class TestInlinesHardLineBreaks + { + // ## Hard line breaks + // + // A line break (not in a code span or HTML tag) that is preceded + // by two or more spaces and does not occur at the end of a block + // is parsed as a [hard line break](@) (rendered + // in HTML as a `
` tag): + [Test] + public void InlinesHardLineBreaks_Example630() + { + // Example 630 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo + // baz + // + // Should be rendered as: + //

foo
+ // baz

+ + Console.WriteLine("Example 630\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo \nbaz", "

foo
\nbaz

", ""); + } + + // For a more visible alternative, a backslash before the + // [line ending] may be used instead of two spaces: + [Test] + public void InlinesHardLineBreaks_Example631() + { + // Example 631 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo\ + // baz + // + // Should be rendered as: + //

foo
+ // baz

+ + Console.WriteLine("Example 631\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo\\\nbaz", "

foo
\nbaz

", ""); + } + + // More than two spaces can be used: + [Test] + public void InlinesHardLineBreaks_Example632() + { + // Example 632 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo + // baz + // + // Should be rendered as: + //

foo
+ // baz

+ + Console.WriteLine("Example 632\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo \nbaz", "

foo
\nbaz

", ""); + } + + // Leading spaces at the beginning of the next line are ignored: + [Test] + public void InlinesHardLineBreaks_Example633() + { + // Example 633 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo + // bar + // + // Should be rendered as: + //

foo
+ // bar

+ + Console.WriteLine("Example 633\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo \n bar", "

foo
\nbar

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example634() + { + // Example 634 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo\ + // bar + // + // Should be rendered as: + //

foo
+ // bar

+ + Console.WriteLine("Example 634\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo\\\n bar", "

foo
\nbar

", ""); + } + + // Line breaks can occur inside emphasis, links, and other constructs + // that allow inline content: + [Test] + public void InlinesHardLineBreaks_Example635() + { + // Example 635 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // *foo + // bar* + // + // Should be rendered as: + //

foo
+ // bar

+ + Console.WriteLine("Example 635\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("*foo \nbar*", "

foo
\nbar

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example636() + { + // Example 636 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // *foo\ + // bar* + // + // Should be rendered as: + //

foo
+ // bar

+ + Console.WriteLine("Example 636\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("*foo\\\nbar*", "

foo
\nbar

", ""); + } + + // Line breaks do not occur inside code spans + [Test] + public void InlinesHardLineBreaks_Example637() + { + // Example 637 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // `code + // span` + // + // Should be rendered as: + //

code span

+ + Console.WriteLine("Example 637\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("`code \nspan`", "

code span

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example638() + { + // Example 638 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // `code\ + // span` + // + // Should be rendered as: + //

code\ span

+ + Console.WriteLine("Example 638\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("`code\\\nspan`", "

code\\ span

", ""); + } + + // or HTML tags: + [Test] + public void InlinesHardLineBreaks_Example639() + { + // Example 639 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + //
+ // + // Should be rendered as: + //

+ + Console.WriteLine("Example 639\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example640() + { + // Example 640 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // + // + // Should be rendered as: + //

+ + Console.WriteLine("Example 640\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("", "

", ""); + } + + // Hard line breaks are for separating inline content within a block. + // Neither syntax for hard line breaks works at the end of a paragraph or + // other block element: + [Test] + public void InlinesHardLineBreaks_Example641() + { + // Example 641 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo\ + // + // Should be rendered as: + //

foo\

+ + Console.WriteLine("Example 641\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo\\", "

foo\\

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example642() + { + // Example 642 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 642\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("foo ", "

foo

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example643() + { + // Example 643 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // ### foo\ + // + // Should be rendered as: + //

foo\

+ + Console.WriteLine("Example 643\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("### foo\\", "

foo\\

", ""); + } + + [Test] + public void InlinesHardLineBreaks_Example644() + { + // Example 644 + // Section: Inlines / Hard line breaks + // + // The following Markdown: + // ### foo + // + // Should be rendered as: + //

foo

+ + Console.WriteLine("Example 644\nSection Inlines / Hard line breaks\n"); + TestRoundtrip.TestSpec("### foo ", "

foo

", ""); + } + } + + [TestFixture] + public class TestInlinesSoftLineBreaks + { + // ## Soft line breaks + // + // A regular line break (not in a code span or HTML tag) that is not + // preceded by two or more spaces or a backslash is parsed as a + // [softbreak](@). (A softbreak may be rendered in HTML either as a + // [line ending] or as a space. The result will be the same in + // browsers. In the examples here, a [line ending] will be used.) + [Test] + public void InlinesSoftLineBreaks_Example645() + { + // Example 645 + // Section: Inlines / Soft line breaks + // + // The following Markdown: + // foo + // baz + // + // Should be rendered as: + //

foo + // baz

+ + Console.WriteLine("Example 645\nSection Inlines / Soft line breaks\n"); + TestRoundtrip.TestSpec("foo\nbaz", "

foo\nbaz

", ""); + } + + // Spaces at the end of the line and beginning of the next line are + // removed: + [Test] + public void InlinesSoftLineBreaks_Example646() + { + // Example 646 + // Section: Inlines / Soft line breaks + // + // The following Markdown: + // foo + // baz + // + // Should be rendered as: + //

foo + // baz

+ + Console.WriteLine("Example 646\nSection Inlines / Soft line breaks\n"); + TestRoundtrip.TestSpec("foo \n baz", "

foo\nbaz

", ""); + } + } + + [TestFixture] + public class TestInlinesTextualContent + { + // A conforming parser may render a soft line break in HTML either as a + // line break or as a space. + // + // A renderer may also provide an option to render soft line breaks + // as hard line breaks. + // + // ## Textual content + // + // Any characters not given an interpretation by the above rules will + // be parsed as plain textual content. + [Test] + public void InlinesTextualContent_Example647() + { + // Example 647 + // Section: Inlines / Textual content + // + // The following Markdown: + // hello $.;'there + // + // Should be rendered as: + //

hello $.;'there

+ + Console.WriteLine("Example 647\nSection Inlines / Textual content\n"); + TestRoundtrip.TestSpec("hello $.;'there", "

hello $.;'there

", ""); + } + + [Test] + public void InlinesTextualContent_Example648() + { + // Example 648 + // Section: Inlines / Textual content + // + // The following Markdown: + // Foo χρῆν + // + // Should be rendered as: + //

Foo χρῆν

+ + Console.WriteLine("Example 648\nSection Inlines / Textual content\n"); + TestRoundtrip.TestSpec("Foo χρῆν", "

Foo χρῆν

", ""); + } + + // Internal spaces are preserved verbatim: + [Test] + public void InlinesTextualContent_Example649() + { + // Example 649 + // Section: Inlines / Textual content + // + // The following Markdown: + // Multiple spaces + // + // Should be rendered as: + //

Multiple spaces

+ + Console.WriteLine("Example 649\nSection Inlines / Textual content\n"); + TestRoundtrip.TestSpec("Multiple spaces", "

Multiple spaces

", ""); + } + // + // + // # Appendix: A parsing strategy + // + // In this appendix we describe some features of the parsing strategy + // used in the CommonMark reference implementations. + // + // ## Overview + // + // Parsing has two phases: + // + // 1. In the first phase, lines of input are consumed and the block + // structure of the document---its division into paragraphs, block quotes, + // list items, and so on---is constructed. Text is assigned to these + // blocks but not parsed. Link reference definitions are parsed and a + // map of links is constructed. + // + // 2. In the second phase, the raw text contents of paragraphs and headings + // are parsed into sequences of Markdown inline elements (strings, + // code spans, links, emphasis, and so on), using the map of link + // references constructed in phase 1. + // + // At each point in processing, the document is represented as a tree of + // **blocks**. The root of the tree is a `document` block. The `document` + // may have any number of other blocks as **children**. These children + // may, in turn, have other blocks as children. The last child of a block + // is normally considered **open**, meaning that subsequent lines of input + // can alter its contents. (Blocks that are not open are **closed**.) + // Here, for example, is a possible document tree, with the open blocks + // marked by arrows: + // + // ``` tree + // -> document + // -> block_quote + // paragraph + // "Lorem ipsum dolor\nsit amet." + // -> list (type=bullet tight=true bullet_char=-) + // list_item + // paragraph + // "Qui *quodsi iracundia*" + // -> list_item + // -> paragraph + // "aliquando id" + // ``` + // + // ## Phase 1: block structure + // + // Each line that is processed has an effect on this tree. The line is + // analyzed and, depending on its contents, the document may be altered + // in one or more of the following ways: + // + // 1. One or more open blocks may be closed. + // 2. One or more new blocks may be created as children of the + // last open block. + // 3. Text may be added to the last (deepest) open block remaining + // on the tree. + // + // Once a line has been incorporated into the tree in this way, + // it can be discarded, so input can be read in a stream. + // + // For each line, we follow this procedure: + // + // 1. First we iterate through the open blocks, starting with the + // root document, and descending through last children down to the last + // open block. Each block imposes a condition that the line must satisfy + // if the block is to remain open. For example, a block quote requires a + // `>` character. A paragraph requires a non-blank line. + // In this phase we may match all or just some of the open + // blocks. But we cannot close unmatched blocks yet, because we may have a + // [lazy continuation line]. + // + // 2. Next, after consuming the continuation markers for existing + // blocks, we look for new block starts (e.g. `>` for a block quote). + // If we encounter a new block start, we close any blocks unmatched + // in step 1 before creating the new block as a child of the last + // matched block. + // + // 3. Finally, we look at the remainder of the line (after block + // markers like `>`, list markers, and indentation have been consumed). + // This is text that can be incorporated into the last open + // block (a paragraph, code block, heading, or raw HTML). + // + // Setext headings are formed when we see a line of a paragraph + // that is a [setext heading underline]. + // + // Reference link definitions are detected when a paragraph is closed; + // the accumulated text lines are parsed to see if they begin with + // one or more reference link definitions. Any remainder becomes a + // normal paragraph. + // + // We can see how this works by considering how the tree above is + // generated by four lines of Markdown: + // + // ``` markdown + // > Lorem ipsum dolor + // sit amet. + // > - Qui *quodsi iracundia* + // > - aliquando id + // ``` + // + // At the outset, our document model is just + // + // ``` tree + // -> document + // ``` + // + // The first line of our text, + // + // ``` markdown + // > Lorem ipsum dolor + // ``` + // + // causes a `block_quote` block to be created as a child of our + // open `document` block, and a `paragraph` block as a child of + // the `block_quote`. Then the text is added to the last open + // block, the `paragraph`: + // + // ``` tree + // -> document + // -> block_quote + // -> paragraph + // "Lorem ipsum dolor" + // ``` + // + // The next line, + // + // ``` markdown + // sit amet. + // ``` + // + // is a "lazy continuation" of the open `paragraph`, so it gets added + // to the paragraph's text: + // + // ``` tree + // -> document + // -> block_quote + // -> paragraph + // "Lorem ipsum dolor\nsit amet." + // ``` + // + // The third line, + // + // ``` markdown + // > - Qui *quodsi iracundia* + // ``` + // + // causes the `paragraph` block to be closed, and a new `list` block + // opened as a child of the `block_quote`. A `list_item` is also + // added as a child of the `list`, and a `paragraph` as a child of + // the `list_item`. The text is then added to the new `paragraph`: + // + // ``` tree + // -> document + // -> block_quote + // paragraph + // "Lorem ipsum dolor\nsit amet." + // -> list (type=bullet tight=true bullet_char=-) + // -> list_item + // -> paragraph + // "Qui *quodsi iracundia*" + // ``` + // + // The fourth line, + // + // ``` markdown + // > - aliquando id + // ``` + // + // causes the `list_item` (and its child the `paragraph`) to be closed, + // and a new `list_item` opened up as child of the `list`. A `paragraph` + // is added as a child of the new `list_item`, to contain the text. + // We thus obtain the final tree: + // + // ``` tree + // -> document + // -> block_quote + // paragraph + // "Lorem ipsum dolor\nsit amet." + // -> list (type=bullet tight=true bullet_char=-) + // list_item + // paragraph + // "Qui *quodsi iracundia*" + // -> list_item + // -> paragraph + // "aliquando id" + // ``` + // + // ## Phase 2: inline structure + // + // Once all of the input has been parsed, all open blocks are closed. + // + // We then "walk the tree," visiting every node, and parse raw + // string contents of paragraphs and headings as inlines. At this + // point we have seen all the link reference definitions, so we can + // resolve reference links as we go. + // + // ``` tree + // document + // block_quote + // paragraph + // str "Lorem ipsum dolor" + // softbreak + // str "sit amet." + // list (type=bullet tight=true bullet_char=-) + // list_item + // paragraph + // str "Qui " + // emph + // str "quodsi iracundia" + // list_item + // paragraph + // str "aliquando id" + // ``` + // + // Notice how the [line ending] in the first paragraph has + // been parsed as a `softbreak`, and the asterisks in the first list item + // have become an `emph`. + // + // ### An algorithm for parsing nested emphasis and links + // + // By far the trickiest part of inline parsing is handling emphasis, + // strong emphasis, links, and images. This is done using the following + // algorithm. + // + // When we're parsing inlines and we hit either + // + // - a run of `*` or `_` characters, or + // - a `[` or `![` + // + // we insert a text node with these symbols as its literal content, and we + // add a pointer to this text node to the [delimiter stack](@). + // + // The [delimiter stack] is a doubly linked list. Each + // element contains a pointer to a text node, plus information about + // + // - the type of delimiter (`[`, `![`, `*`, `_`) + // - the number of delimiters, + // - whether the delimiter is "active" (all are active to start), and + // - whether the delimiter is a potential opener, a potential closer, + // or both (which depends on what sort of characters precede + // and follow the delimiters). + // + // When we hit a `]` character, we call the *look for link or image* + // procedure (see below). + // + // When we hit the end of the input, we call the *process emphasis* + // procedure (see below), with `stack_bottom` = NULL. + // + // #### *look for link or image* + // + // Starting at the top of the delimiter stack, we look backwards + // through the stack for an opening `[` or `![` delimiter. + // + // - If we don't find one, we return a literal text node `]`. + // + // - If we do find one, but it's not *active*, we remove the inactive + // delimiter from the stack, and return a literal text node `]`. + // + // - If we find one and it's active, then we parse ahead to see if + // we have an inline link/image, reference link/image, compact reference + // link/image, or shortcut reference link/image. + // + // + If we don't, then we remove the opening delimiter from the + // delimiter stack and return a literal text node `]`. + // + // + If we do, then + // + // * We return a link or image node whose children are the inlines + // after the text node pointed to by the opening delimiter. + // + // * We run *process emphasis* on these inlines, with the `[` opener + // as `stack_bottom`. + // + // * We remove the opening delimiter. + // + // * If we have a link (and not an image), we also set all + // `[` delimiters before the opening delimiter to *inactive*. (This + // will prevent us from getting links within links.) + // + // #### *process emphasis* + // + // Parameter `stack_bottom` sets a lower bound to how far we + // descend in the [delimiter stack]. If it is NULL, we can + // go all the way to the bottom. Otherwise, we stop before + // visiting `stack_bottom`. + // + // Let `current_position` point to the element on the [delimiter stack] + // just above `stack_bottom` (or the first element if `stack_bottom` + // is NULL). + // + // We keep track of the `openers_bottom` for each delimiter + // type (`*`, `_`) and each length of the closing delimiter run + // (modulo 3). Initialize this to `stack_bottom`. + // + // Then we repeat the following until we run out of potential + // closers: + // + // - Move `current_position` forward in the delimiter stack (if needed) + // until we find the first potential closer with delimiter `*` or `_`. + // (This will be the potential closer closest + // to the beginning of the input -- the first one in parse order.) + // + // - Now, look back in the stack (staying above `stack_bottom` and + // the `openers_bottom` for this delimiter type) for the + // first matching potential opener ("matching" means same delimiter). + // + // - If one is found: + // + // + Figure out whether we have emphasis or strong emphasis: + // if both closer and opener spans have length >= 2, we have + // strong, otherwise regular. + // + // + Insert an emph or strong emph node accordingly, after + // the text node corresponding to the opener. + // + // + Remove any delimiters between the opener and closer from + // the delimiter stack. + // + // + Remove 1 (for regular emph) or 2 (for strong emph) delimiters + // from the opening and closing text nodes. If they become empty + // as a result, remove them and remove the corresponding element + // of the delimiter stack. If the closing node is removed, reset + // `current_position` to the next element in the stack. + // + // - If none is found: + // + // + Set `openers_bottom` to the element before `current_position`. + // (We know that there are no openers for this kind of closer up to and + // including this point, so this puts a lower bound on future searches.) + // + // + If the closer at `current_position` is not a potential opener, + // remove it from the delimiter stack (since we know it can't + // be a closer either). + // + // + Advance `current_position` to the next element in the stack. + // + // After we're done, we remove all delimiters above `stack_bottom` from the + // delimiter stack. + } +} diff --git a/src/Markdig.Tests/RoundtripSpecs/CommonMark.md b/src/Markdig.Tests/RoundtripSpecs/CommonMark.md new file mode 100644 index 000000000..e828e314a --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/CommonMark.md @@ -0,0 +1,9710 @@ +--- +title: CommonMark Spec +author: John MacFarlane +version: 0.29 +date: '2019-04-06' +license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)' +... + +# Introduction + +## What is Markdown? + +Markdown is a plain text format for writing structured documents, +based on conventions for indicating formatting in email +and usenet posts. It was developed by John Gruber (with +help from Aaron Swartz) and released in 2004 in the form of a +[syntax description](http://daringfireball.net/projects/markdown/syntax) +and a Perl script (`Markdown.pl`) for converting Markdown to +HTML. In the next decade, dozens of implementations were +developed in many languages. Some extended the original +Markdown syntax with conventions for footnotes, tables, and +other document elements. Some allowed Markdown documents to be +rendered in formats other than HTML. Websites like Reddit, +StackOverflow, and GitHub had millions of people using Markdown. +And Markdown started to be used beyond the web, to author books, +articles, slide shows, letters, and lecture notes. + +What distinguishes Markdown from many other lightweight markup +syntaxes, which are often easier to write, is its readability. +As Gruber writes: + +> The overriding design goal for Markdown's formatting syntax is +> to make it as readable as possible. The idea is that a +> Markdown-formatted document should be publishable as-is, as +> plain text, without looking like it's been marked up with tags +> or formatting instructions. +> () + +The point can be illustrated by comparing a sample of +[AsciiDoc](http://www.methods.co.nz/asciidoc/) with +an equivalent sample of Markdown. Here is a sample of +AsciiDoc from the AsciiDoc manual: + +``` +1. List item one. ++ +List item one continued with a second paragraph followed by an +Indented block. ++ +................. +$ ls *.sh +$ mv *.sh ~/tmp +................. ++ +List item continued with a third paragraph. + +2. List item two continued with an open block. ++ +-- +This paragraph is part of the preceding list item. + +a. This list is nested and does not require explicit item +continuation. ++ +This paragraph is part of the preceding list item. + +b. List item b. + +This paragraph belongs to item two of the outer list. +-- +``` + +And here is the equivalent in Markdown: +``` +1. List item one. + + List item one continued with a second paragraph followed by an + Indented block. + + $ ls *.sh + $ mv *.sh ~/tmp + + List item continued with a third paragraph. + +2. List item two continued with an open block. + + This paragraph is part of the preceding list item. + + 1. This list is nested and does not require explicit item continuation. + + This paragraph is part of the preceding list item. + + 2. List item b. + + This paragraph belongs to item two of the outer list. +``` + +The AsciiDoc version is, arguably, easier to write. You don't need +to worry about indentation. But the Markdown version is much easier +to read. The nesting of list items is apparent to the eye in the +source, not just in the processed document. + +## Why is a spec needed? + +John Gruber's [canonical description of Markdown's +syntax](http://daringfireball.net/projects/markdown/syntax) +does not specify the syntax unambiguously. Here are some examples of +questions it does not answer: + +1. How much indentation is needed for a sublist? The spec says that + continuation paragraphs need to be indented four spaces, but is + not fully explicit about sublists. It is natural to think that + they, too, must be indented four spaces, but `Markdown.pl` does + not require that. This is hardly a "corner case," and divergences + between implementations on this issue often lead to surprises for + users in real documents. (See [this comment by John + Gruber](http://article.gmane.org/gmane.text.markdown.general/1997).) + +2. Is a blank line needed before a block quote or heading? + Most implementations do not require the blank line. However, + this can lead to unexpected results in hard-wrapped text, and + also to ambiguities in parsing (note that some implementations + put the heading inside the blockquote, while others do not). + (John Gruber has also spoken [in favor of requiring the blank + lines](http://article.gmane.org/gmane.text.markdown.general/2146).) + +3. Is a blank line needed before an indented code block? + (`Markdown.pl` requires it, but this is not mentioned in the + documentation, and some implementations do not require it.) + + ``` markdown + paragraph + code? + ``` + +4. What is the exact rule for determining when list items get + wrapped in `

` tags? Can a list be partially "loose" and partially + "tight"? What should we do with a list like this? + + ``` markdown + 1. one + + 2. two + 3. three + ``` + + Or this? + + ``` markdown + 1. one + - a + + - b + 2. two + ``` + + (There are some relevant comments by John Gruber + [here](http://article.gmane.org/gmane.text.markdown.general/2554).) + +5. Can list markers be indented? Can ordered list markers be right-aligned? + + ``` markdown + 8. item 1 + 9. item 2 + 10. item 2a + ``` + +6. Is this one list with a thematic break in its second item, + or two lists separated by a thematic break? + + ``` markdown + * a + * * * * * + * b + ``` + +7. When list markers change from numbers to bullets, do we have + two lists or one? (The Markdown syntax description suggests two, + but the perl scripts and many other implementations produce one.) + + ``` markdown + 1. fee + 2. fie + - foe + - fum + ``` + +8. What are the precedence rules for the markers of inline structure? + For example, is the following a valid link, or does the code span + take precedence ? + + ``` markdown + [a backtick (`)](/url) and [another backtick (`)](/url). + ``` + +9. What are the precedence rules for markers of emphasis and strong + emphasis? For example, how should the following be parsed? + + ``` markdown + *foo *bar* baz* + ``` + +10. What are the precedence rules between block-level and inline-level + structure? For example, how should the following be parsed? + + ``` markdown + - `a long code span can contain a hyphen like this + - and it can screw things up` + ``` + +11. Can list items include section headings? (`Markdown.pl` does not + allow this, but does allow blockquotes to include headings.) + + ``` markdown + - # Heading + ``` + +12. Can list items be empty? + + ``` markdown + * a + * + * b + ``` + +13. Can link references be defined inside block quotes or list items? + + ``` markdown + > Blockquote [foo]. + > + > [foo]: /url + ``` + +14. If there are multiple definitions for the same reference, which takes + precedence? + + ``` markdown + [foo]: /url1 + [foo]: /url2 + + [foo][] + ``` + +In the absence of a spec, early implementers consulted `Markdown.pl` +to resolve these ambiguities. But `Markdown.pl` was quite buggy, and +gave manifestly bad results in many cases, so it was not a +satisfactory replacement for a spec. + +Because there is no unambiguous spec, implementations have diverged +considerably. As a result, users are often surprised to find that +a document that renders one way on one system (say, a GitHub wiki) +renders differently on another (say, converting to docbook using +pandoc). To make matters worse, because nothing in Markdown counts +as a "syntax error," the divergence often isn't discovered right away. + +## About this document + +This document attempts to specify Markdown syntax unambiguously. +It contains many examples with side-by-side Markdown and +HTML. These are intended to double as conformance tests. An +accompanying script `spec_tests.py` can be used to run the tests +against any Markdown program: + + python test/spec_tests.py --spec spec.txt --program PROGRAM + +Since this document describes how Markdown is to be parsed into +an abstract syntax tree, it would have made sense to use an abstract +representation of the syntax tree instead of HTML. But HTML is capable +of representing the structural distinctions we need to make, and the +choice of HTML for the tests makes it possible to run the tests against +an implementation without writing an abstract syntax tree renderer. + +This document is generated from a text file, `spec.txt`, written +in Markdown with a small extension for the side-by-side tests. +The script `tools/makespec.py` can be used to convert `spec.txt` into +HTML or CommonMark (which can then be converted into other formats). + +In the examples, the `→` character is used to represent tabs. + +# Preliminaries + +## Characters and lines + +Any sequence of [characters] is a valid CommonMark +document. + +A [character](@) is a Unicode code point. Although some +code points (for example, combining accents) do not correspond to +characters in an intuitive sense, all code points count as characters +for purposes of this spec. + +This spec does not specify an encoding; it thinks of lines as composed +of [characters] rather than bytes. A conforming parser may be limited +to a certain encoding. + +A [line](@) is a sequence of zero or more [characters] +other than newline (`U+000A`) or carriage return (`U+000D`), +followed by a [line ending] or by the end of file. + +A [line ending](@) is a newline (`U+000A`), a carriage return +(`U+000D`) not followed by a newline, or a carriage return and a +following newline. + +A line containing no characters, or a line containing only spaces +(`U+0020`) or tabs (`U+0009`), is called a [blank line](@). + +The following definitions of character classes will be used in this spec: + +A [whitespace character](@) is a space +(`U+0020`), tab (`U+0009`), newline (`U+000A`), line tabulation (`U+000B`), +form feed (`U+000C`), or carriage return (`U+000D`). + +[Whitespace](@) is a sequence of one or more [whitespace +characters]. + +A [Unicode whitespace character](@) is +any code point in the Unicode `Zs` general category, or a tab (`U+0009`), +carriage return (`U+000D`), newline (`U+000A`), or form feed +(`U+000C`). + +[Unicode whitespace](@) is a sequence of one +or more [Unicode whitespace characters]. + +A [space](@) is `U+0020`. + +A [non-whitespace character](@) is any character +that is not a [whitespace character]. + +An [ASCII punctuation character](@) +is `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, +`*`, `+`, `,`, `-`, `.`, `/` (U+0021–2F), +`:`, `;`, `<`, `=`, `>`, `?`, `@` (U+003A–0040), +`[`, `\`, `]`, `^`, `_`, `` ` `` (U+005B–0060), +`{`, `|`, `}`, or `~` (U+007B–007E). + +A [punctuation character](@) is an [ASCII +punctuation character] or anything in +the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or `Ps`. + +## Tabs + +Tabs in lines are not expanded to [spaces]. However, +in contexts where whitespace helps to define block structure, +tabs behave as if they were replaced by spaces with a tab stop +of 4 characters. + +Thus, for example, a tab can be used instead of four spaces +in an indented code block. (Note, however, that internal +tabs are passed through as literal tabs, not expanded to +spaces.) + +```````````````````````````````` example +→foo→baz→→bim +. +

foo→baz→→bim
+
+```````````````````````````````` + +```````````````````````````````` example + →foo→baz→→bim +. +
foo→baz→→bim
+
+```````````````````````````````` + +```````````````````````````````` example + a→a + ὐ→a +. +
a→a
+ὐ→a
+
+```````````````````````````````` + +In the following example, a continuation paragraph of a list +item is indented with a tab; this has exactly the same effect +as indentation with four spaces would: + +```````````````````````````````` example + - foo + +→bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + +```````````````````````````````` example +- foo + +→→bar +. +
    +
  • +

    foo

    +
      bar
    +
    +
  • +
+```````````````````````````````` + +Normally the `>` that begins a block quote may be followed +optionally by a space, which is not considered part of the +content. In the following case `>` is followed by a tab, +which is treated as if it were expanded into three spaces. +Since one of these spaces is considered part of the +delimiter, `foo` is considered to be indented six spaces +inside the block quote context, so we get an indented +code block starting with two spaces. + +```````````````````````````````` example +>→→foo +. +
+
  foo
+
+
+```````````````````````````````` + +```````````````````````````````` example +-→→foo +. +
    +
  • +
      foo
    +
    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example + foo +→bar +. +
foo
+bar
+
+```````````````````````````````` + +```````````````````````````````` example + - foo + - bar +→ - baz +. +
    +
  • foo +
      +
    • bar +
        +
      • baz
      • +
      +
    • +
    +
  • +
+```````````````````````````````` + +```````````````````````````````` example +#→Foo +. +

Foo

+```````````````````````````````` + +```````````````````````````````` example +*→*→*→ +. +
+```````````````````````````````` + + +## Insecure characters + +For security reasons, the Unicode character `U+0000` must be replaced +with the REPLACEMENT CHARACTER (`U+FFFD`). + +# Blocks and inlines + +We can think of a document as a sequence of +[blocks](@)---structural elements like paragraphs, block +quotations, lists, headings, rules, and code blocks. Some blocks (like +block quotes and list items) contain other blocks; others (like +headings and paragraphs) contain [inline](@) content---text, +links, emphasized text, images, code spans, and so on. + +## Precedence + +Indicators of block structure always take precedence over indicators +of inline structure. So, for example, the following is a list with +two items, not a list with one item containing a code span: + +```````````````````````````````` example +- `one +- two` +. +
    +
  • `one
  • +
  • two`
  • +
+```````````````````````````````` + + +This means that parsing can proceed in two steps: first, the block +structure of the document can be discerned; second, text lines inside +paragraphs, headings, and other block constructs can be parsed for inline +structure. The second step requires information about link reference +definitions that will be available only at the end of the first +step. Note that the first step requires processing lines in sequence, +but the second can be parallelized, since the inline parsing of +one block element does not affect the inline parsing of any other. + +## Container blocks and leaf blocks + +We can divide blocks into two types: +[container blocks](@), +which can contain other blocks, and [leaf blocks](@), +which cannot. + +# Leaf blocks + +This section describes the different kinds of leaf block that make up a +Markdown document. + +## Thematic breaks + +A line consisting of 0-3 spaces of indentation, followed by a sequence +of three or more matching `-`, `_`, or `*` characters, each followed +optionally by any number of spaces or tabs, forms a +[thematic break](@). + +```````````````````````````````` example +*** +--- +___ +. +
+
+
+```````````````````````````````` + + +Wrong characters: + +```````````````````````````````` example ++++ +. +

+++

+```````````````````````````````` + + +```````````````````````````````` example +=== +. +

===

+```````````````````````````````` + + +Not enough characters: + +```````````````````````````````` example +-- +** +__ +. +

-- +** +__

+```````````````````````````````` + + +One to three spaces indent are allowed: + +```````````````````````````````` example + *** + *** + *** +. +
+
+
+```````````````````````````````` + + +Four spaces is too many: + +```````````````````````````````` example + *** +. +
***
+
+```````````````````````````````` + + +```````````````````````````````` example +Foo + *** +. +

Foo +***

+```````````````````````````````` + + +More than three characters may be used: + +```````````````````````````````` example +_____________________________________ +. +
+```````````````````````````````` + + +Spaces are allowed between the characters: + +```````````````````````````````` example + - - - +. +
+```````````````````````````````` + + +```````````````````````````````` example + ** * ** * ** * ** +. +
+```````````````````````````````` + + +```````````````````````````````` example +- - - - +. +
+```````````````````````````````` + + +Spaces are allowed at the end: + +```````````````````````````````` example +- - - - +. +
+```````````````````````````````` + + +However, no other characters may occur in the line: + +```````````````````````````````` example +_ _ _ _ a + +a------ + +---a--- +. +

_ _ _ _ a

+

a------

+

---a---

+```````````````````````````````` + + +It is required that all of the [non-whitespace characters] be the same. +So, this is not a thematic break: + +```````````````````````````````` example + *-* +. +

-

+```````````````````````````````` + + +Thematic breaks do not need blank lines before or after: + +```````````````````````````````` example +- foo +*** +- bar +. +
    +
  • foo
  • +
+
+
    +
  • bar
  • +
+```````````````````````````````` + + +Thematic breaks can interrupt a paragraph: + +```````````````````````````````` example +Foo +*** +bar +. +

Foo

+
+

bar

+```````````````````````````````` + + +If a line of dashes that meets the above conditions for being a +thematic break could also be interpreted as the underline of a [setext +heading], the interpretation as a +[setext heading] takes precedence. Thus, for example, +this is a setext heading, not a paragraph followed by a thematic break: + +```````````````````````````````` example +Foo +--- +bar +. +

Foo

+

bar

+```````````````````````````````` + + +When both a thematic break and a list item are possible +interpretations of a line, the thematic break takes precedence: + +```````````````````````````````` example +* Foo +* * * +* Bar +. +
    +
  • Foo
  • +
+
+
    +
  • Bar
  • +
+```````````````````````````````` + + +If you want a thematic break in a list item, use a different bullet: + +```````````````````````````````` example +- Foo +- * * * +. +
    +
  • Foo
  • +
  • +
    +
  • +
+```````````````````````````````` + + +## ATX headings + +An [ATX heading](@) +consists of a string of characters, parsed as inline content, between an +opening sequence of 1--6 unescaped `#` characters and an optional +closing sequence of any number of unescaped `#` characters. +The opening sequence of `#` characters must be followed by a +[space] or by the end of line. The optional closing sequence of `#`s must be +preceded by a [space] and may be followed by spaces only. The opening +`#` character may be indented 0-3 spaces. The raw contents of the +heading are stripped of leading and trailing spaces before being parsed +as inline content. The heading level is equal to the number of `#` +characters in the opening sequence. + +Simple headings: + +```````````````````````````````` example +# foo +## foo +### foo +#### foo +##### foo +###### foo +. +

foo

+

foo

+

foo

+

foo

+
foo
+
foo
+```````````````````````````````` + + +More than six `#` characters is not a heading: + +```````````````````````````````` example +####### foo +. +

####### foo

+```````````````````````````````` + + +At least one space is required between the `#` characters and the +heading's contents, unless the heading is empty. Note that many +implementations currently do not require the space. However, the +space was required by the +[original ATX implementation](http://www.aaronsw.com/2002/atx/atx.py), +and it helps prevent things like the following from being parsed as +headings: + +```````````````````````````````` example +#5 bolt + +#hashtag +. +

#5 bolt

+

#hashtag

+```````````````````````````````` + + +This is not a heading, because the first `#` is escaped: + +```````````````````````````````` example +\## foo +. +

## foo

+```````````````````````````````` + + +Contents are parsed as inlines: + +```````````````````````````````` example +# foo *bar* \*baz\* +. +

foo bar *baz*

+```````````````````````````````` + + +Leading and trailing [whitespace] is ignored in parsing inline content: + +```````````````````````````````` example +# foo +. +

foo

+```````````````````````````````` + + +One to three spaces indentation are allowed: + +```````````````````````````````` example + ### foo + ## foo + # foo +. +

foo

+

foo

+

foo

+```````````````````````````````` + + +Four spaces are too much: + +```````````````````````````````` example + # foo +. +
# foo
+
+```````````````````````````````` + + +```````````````````````````````` example +foo + # bar +. +

foo +# bar

+```````````````````````````````` + + +A closing sequence of `#` characters is optional: + +```````````````````````````````` example +## foo ## + ### bar ### +. +

foo

+

bar

+```````````````````````````````` + + +It need not be the same length as the opening sequence: + +```````````````````````````````` example +# foo ################################## +##### foo ## +. +

foo

+
foo
+```````````````````````````````` + + +Spaces are allowed after the closing sequence: + +```````````````````````````````` example +### foo ### +. +

foo

+```````````````````````````````` + + +A sequence of `#` characters with anything but [spaces] following it +is not a closing sequence, but counts as part of the contents of the +heading: + +```````````````````````````````` example +### foo ### b +. +

foo ### b

+```````````````````````````````` + + +The closing sequence must be preceded by a space: + +```````````````````````````````` example +# foo# +. +

foo#

+```````````````````````````````` + + +Backslash-escaped `#` characters do not count as part +of the closing sequence: + +```````````````````````````````` example +### foo \### +## foo #\## +# foo \# +. +

foo ###

+

foo ###

+

foo #

+```````````````````````````````` + + +ATX headings need not be separated from surrounding content by blank +lines, and they can interrupt paragraphs: + +```````````````````````````````` example +**** +## foo +**** +. +
+

foo

+
+```````````````````````````````` + + +```````````````````````````````` example +Foo bar +# baz +Bar foo +. +

Foo bar

+

baz

+

Bar foo

+```````````````````````````````` + + +ATX headings can be empty: + +```````````````````````````````` example +## +# +### ### +. +

+

+

+```````````````````````````````` + + +## Setext headings + +A [setext heading](@) consists of one or more +lines of text, each containing at least one [non-whitespace +character], with no more than 3 spaces indentation, followed by +a [setext heading underline]. The lines of text must be such +that, were they not followed by the setext heading underline, +they would be interpreted as a paragraph: they cannot be +interpretable as a [code fence], [ATX heading][ATX headings], +[block quote][block quotes], [thematic break][thematic breaks], +[list item][list items], or [HTML block][HTML blocks]. + +A [setext heading underline](@) is a sequence of +`=` characters or a sequence of `-` characters, with no more than 3 +spaces indentation and any number of trailing spaces. If a line +containing a single `-` can be interpreted as an +empty [list items], it should be interpreted this way +and not as a [setext heading underline]. + +The heading is a level 1 heading if `=` characters are used in +the [setext heading underline], and a level 2 heading if `-` +characters are used. The contents of the heading are the result +of parsing the preceding lines of text as CommonMark inline +content. + +In general, a setext heading need not be preceded or followed by a +blank line. However, it cannot interrupt a paragraph, so when a +setext heading comes after a paragraph, a blank line is needed between +them. + +Simple examples: + +```````````````````````````````` example +Foo *bar* +========= + +Foo *bar* +--------- +. +

Foo bar

+

Foo bar

+```````````````````````````````` + + +The content of the header may span more than one line: + +```````````````````````````````` example +Foo *bar +baz* +==== +. +

Foo bar +baz

+```````````````````````````````` + +The contents are the result of parsing the headings's raw +content as inlines. The heading's raw content is formed by +concatenating the lines and removing initial and final +[whitespace]. + +```````````````````````````````` example + Foo *bar +baz*→ +==== +. +

Foo bar +baz

+```````````````````````````````` + + +The underlining can be any length: + +```````````````````````````````` example +Foo +------------------------- + +Foo += +. +

Foo

+

Foo

+```````````````````````````````` + + +The heading content can be indented up to three spaces, and need +not line up with the underlining: + +```````````````````````````````` example + Foo +--- + + Foo +----- + + Foo + === +. +

Foo

+

Foo

+

Foo

+```````````````````````````````` + + +Four spaces indent is too much: + +```````````````````````````````` example + Foo + --- + + Foo +--- +. +
Foo
+---
+
+Foo
+
+
+```````````````````````````````` + + +The setext heading underline can be indented up to three spaces, and +may have trailing spaces: + +```````````````````````````````` example +Foo + ---- +. +

Foo

+```````````````````````````````` + + +Four spaces is too much: + +```````````````````````````````` example +Foo + --- +. +

Foo +---

+```````````````````````````````` + + +The setext heading underline cannot contain internal spaces: + +```````````````````````````````` example +Foo += = + +Foo +--- - +. +

Foo += =

+

Foo

+
+```````````````````````````````` + + +Trailing spaces in the content line do not cause a line break: + +```````````````````````````````` example +Foo +----- +. +

Foo

+```````````````````````````````` + + +Nor does a backslash at the end: + +```````````````````````````````` example +Foo\ +---- +. +

Foo\

+```````````````````````````````` + + +Since indicators of block structure take precedence over +indicators of inline structure, the following are setext headings: + +```````````````````````````````` example +`Foo +---- +` + +
+. +

`Foo

+

`

+

<a title="a lot

+

of dashes"/>

+```````````````````````````````` + + +The setext heading underline cannot be a [lazy continuation +line] in a list item or block quote: + +```````````````````````````````` example +> Foo +--- +. +
+

Foo

+
+
+```````````````````````````````` + + +```````````````````````````````` example +> foo +bar +=== +. +
+

foo +bar +===

+
+```````````````````````````````` + + +```````````````````````````````` example +- Foo +--- +. +
    +
  • Foo
  • +
+
+```````````````````````````````` + + +A blank line is needed between a paragraph and a following +setext heading, since otherwise the paragraph becomes part +of the heading's content: + +```````````````````````````````` example +Foo +Bar +--- +. +

Foo +Bar

+```````````````````````````````` + + +But in general a blank line is not required before or after +setext headings: + +```````````````````````````````` example +--- +Foo +--- +Bar +--- +Baz +. +
+

Foo

+

Bar

+

Baz

+```````````````````````````````` + + +Setext headings cannot be empty: + +```````````````````````````````` example + +==== +. +

====

+```````````````````````````````` + + +Setext heading text lines must not be interpretable as block +constructs other than paragraphs. So, the line of dashes +in these examples gets interpreted as a thematic break: + +```````````````````````````````` example +--- +--- +. +
+
+```````````````````````````````` + + +```````````````````````````````` example +- foo +----- +. +
    +
  • foo
  • +
+
+```````````````````````````````` + + +```````````````````````````````` example + foo +--- +. +
foo
+
+
+```````````````````````````````` + + +```````````````````````````````` example +> foo +----- +. +
+

foo

+
+
+```````````````````````````````` + + +If you want a heading with `> foo` as its literal text, you can +use backslash escapes: + +```````````````````````````````` example +\> foo +------ +. +

> foo

+```````````````````````````````` + + +**Compatibility note:** Most existing Markdown implementations +do not allow the text of setext headings to span multiple lines. +But there is no consensus about how to interpret + +``` markdown +Foo +bar +--- +baz +``` + +One can find four different interpretations: + +1. paragraph "Foo", heading "bar", paragraph "baz" +2. paragraph "Foo bar", thematic break, paragraph "baz" +3. paragraph "Foo bar --- baz" +4. heading "Foo bar", paragraph "baz" + +We find interpretation 4 most natural, and interpretation 4 +increases the expressive power of CommonMark, by allowing +multiline headings. Authors who want interpretation 1 can +put a blank line after the first paragraph: + +```````````````````````````````` example +Foo + +bar +--- +baz +. +

Foo

+

bar

+

baz

+```````````````````````````````` + + +Authors who want interpretation 2 can put blank lines around +the thematic break, + +```````````````````````````````` example +Foo +bar + +--- + +baz +. +

Foo +bar

+
+

baz

+```````````````````````````````` + + +or use a thematic break that cannot count as a [setext heading +underline], such as + +```````````````````````````````` example +Foo +bar +* * * +baz +. +

Foo +bar

+
+

baz

+```````````````````````````````` + + +Authors who want interpretation 3 can use backslash escapes: + +```````````````````````````````` example +Foo +bar +\--- +baz +. +

Foo +bar +--- +baz

+```````````````````````````````` + + +## Indented code blocks + +An [indented code block](@) is composed of one or more +[indented chunks] separated by blank lines. +An [indented chunk](@) is a sequence of non-blank lines, +each indented four or more spaces. The contents of the code block are +the literal contents of the lines, including trailing +[line endings], minus four spaces of indentation. +An indented code block has no [info string]. + +An indented code block cannot interrupt a paragraph, so there must be +a blank line between a paragraph and a following indented code block. +(A blank line is not needed, however, between a code block and a following +paragraph.) + +```````````````````````````````` example + a simple + indented code block +. +
a simple
+  indented code block
+
+```````````````````````````````` + + +If there is any ambiguity between an interpretation of indentation +as a code block and as indicating that material belongs to a [list +item][list items], the list item interpretation takes precedence: + +```````````````````````````````` example + - foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. foo + + - bar +. +
    +
  1. +

    foo

    +
      +
    • bar
    • +
    +
  2. +
+```````````````````````````````` + + + +The contents of a code block are literal text, and do not get parsed +as Markdown: + +```````````````````````````````` example +
+ *hi* + + - one +. +
<a/>
+*hi*
+
+- one
+
+```````````````````````````````` + + +Here we have three chunks separated by blank lines: + +```````````````````````````````` example + chunk1 + + chunk2 + + + + chunk3 +. +
chunk1
+
+chunk2
+
+
+
+chunk3
+
+```````````````````````````````` + + +Any initial spaces beyond four will be included in the content, even +in interior blank lines: + +```````````````````````````````` example + chunk1 + + chunk2 +. +
chunk1
+  
+  chunk2
+
+```````````````````````````````` + + +An indented code block cannot interrupt a paragraph. (This +allows hanging indents and the like.) + +```````````````````````````````` example +Foo + bar + +. +

Foo +bar

+```````````````````````````````` + + +However, any non-blank line with fewer than four leading spaces ends +the code block immediately. So a paragraph may occur immediately +after indented code: + +```````````````````````````````` example + foo +bar +. +
foo
+
+

bar

+```````````````````````````````` + + +And indented code can occur immediately before and after other kinds of +blocks: + +```````````````````````````````` example +# Heading + foo +Heading +------ + foo +---- +. +

Heading

+
foo
+
+

Heading

+
foo
+
+
+```````````````````````````````` + + +The first line can be indented more than four spaces: + +```````````````````````````````` example + foo + bar +. +
    foo
+bar
+
+```````````````````````````````` + + +Blank lines preceding or following an indented code block +are not included in it: + +```````````````````````````````` example + + + foo + + +. +
foo
+
+```````````````````````````````` + + +Trailing spaces are included in the code block's content: + +```````````````````````````````` example + foo +. +
foo  
+
+```````````````````````````````` + + + +## Fenced code blocks + +A [code fence](@) is a sequence +of at least three consecutive backtick characters (`` ` ``) or +tildes (`~`). (Tildes and backticks cannot be mixed.) +A [fenced code block](@) +begins with a code fence, indented no more than three spaces. + +The line with the opening code fence may optionally contain some text +following the code fence; this is trimmed of leading and trailing +whitespace and called the [info string](@). If the [info string] comes +after a backtick fence, it may not contain any backtick +characters. (The reason for this restriction is that otherwise +some inline code would be incorrectly interpreted as the +beginning of a fenced code block.) + +The content of the code block consists of all subsequent lines, until +a closing [code fence] of the same type as the code block +began with (backticks or tildes), and with at least as many backticks +or tildes as the opening code fence. If the leading code fence is +indented N spaces, then up to N spaces of indentation are removed from +each line of the content (if present). (If a content line is not +indented, it is preserved unchanged. If it is indented less than N +spaces, all of the indentation is removed.) + +The closing code fence may be indented up to three spaces, and may be +followed only by spaces, which are ignored. If the end of the +containing block (or document) is reached and no closing code fence +has been found, the code block contains all of the lines after the +opening code fence until the end of the containing block (or +document). (An alternative spec would require backtracking in the +event that a closing code fence is not found. But this makes parsing +much less efficient, and there seems to be no real down side to the +behavior described here.) + +A fenced code block may interrupt a paragraph, and does not require +a blank line either before or after. + +The content of a code fence is treated as literal text, not parsed +as inlines. The first word of the [info string] is typically used to +specify the language of the code sample, and rendered in the `class` +attribute of the `code` tag. However, this spec does not mandate any +particular treatment of the [info string]. + +Here is a simple example with backticks: + +```````````````````````````````` example +``` +< + > +``` +. +
<
+ >
+
+```````````````````````````````` + + +With tildes: + +```````````````````````````````` example +~~~ +< + > +~~~ +. +
<
+ >
+
+```````````````````````````````` + +Fewer than three backticks is not enough: + +```````````````````````````````` example +`` +foo +`` +. +

foo

+```````````````````````````````` + +The closing code fence must use the same character as the opening +fence: + +```````````````````````````````` example +``` +aaa +~~~ +``` +. +
aaa
+~~~
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~ +aaa +``` +~~~ +. +
aaa
+```
+
+```````````````````````````````` + + +The closing code fence must be at least as long as the opening fence: + +```````````````````````````````` example +```` +aaa +``` +`````` +. +
aaa
+```
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~~ +aaa +~~~ +~~~~ +. +
aaa
+~~~
+
+```````````````````````````````` + + +Unclosed code blocks are closed by the end of the document +(or the enclosing [block quote][block quotes] or [list item][list items]): + +```````````````````````````````` example +``` +. +
+```````````````````````````````` + + +```````````````````````````````` example +````` + +``` +aaa +. +

+```
+aaa
+
+```````````````````````````````` + + +```````````````````````````````` example +> ``` +> aaa + +bbb +. +
+
aaa
+
+
+

bbb

+```````````````````````````````` + + +A code block can have all empty lines as its content: + +```````````````````````````````` example +``` + + +``` +. +

+  
+
+```````````````````````````````` + + +A code block can be empty: + +```````````````````````````````` example +``` +``` +. +
+```````````````````````````````` + + +Fences can be indented. If the opening fence is indented, +content lines will have equivalent opening indentation removed, +if present: + +```````````````````````````````` example + ``` + aaa +aaa +``` +. +
aaa
+aaa
+
+```````````````````````````````` + + +```````````````````````````````` example + ``` +aaa + aaa +aaa + ``` +. +
aaa
+aaa
+aaa
+
+```````````````````````````````` + + +```````````````````````````````` example + ``` + aaa + aaa + aaa + ``` +. +
aaa
+ aaa
+aaa
+
+```````````````````````````````` + + +Four spaces indentation produces an indented code block: + +```````````````````````````````` example + ``` + aaa + ``` +. +
```
+aaa
+```
+
+```````````````````````````````` + + +Closing fences may be indented by 0-3 spaces, and their indentation +need not match that of the opening fence: + +```````````````````````````````` example +``` +aaa + ``` +. +
aaa
+
+```````````````````````````````` + + +```````````````````````````````` example + ``` +aaa + ``` +. +
aaa
+
+```````````````````````````````` + + +This is not a closing fence, because it is indented 4 spaces: + +```````````````````````````````` example +``` +aaa + ``` +. +
aaa
+    ```
+
+```````````````````````````````` + + + +Code fences (opening and closing) cannot contain internal spaces: + +```````````````````````````````` example +``` ``` +aaa +. +

+aaa

+```````````````````````````````` + + +```````````````````````````````` example +~~~~~~ +aaa +~~~ ~~ +. +
aaa
+~~~ ~~
+
+```````````````````````````````` + + +Fenced code blocks can interrupt paragraphs, and can be followed +directly by paragraphs, without a blank line between: + +```````````````````````````````` example +foo +``` +bar +``` +baz +. +

foo

+
bar
+
+

baz

+```````````````````````````````` + + +Other blocks can also occur before and after fenced code blocks +without an intervening blank line: + +```````````````````````````````` example +foo +--- +~~~ +bar +~~~ +# baz +. +

foo

+
bar
+
+

baz

+```````````````````````````````` + + +An [info string] can be provided after the opening code fence. +Although this spec doesn't mandate any particular treatment of +the info string, the first word is typically used to specify +the language of the code block. In HTML output, the language is +normally indicated by adding a class to the `code` element consisting +of `language-` followed by the language name. + +```````````````````````````````` example +```ruby +def foo(x) + return 3 +end +``` +. +
def foo(x)
+  return 3
+end
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~~ ruby startline=3 $%@#$ +def foo(x) + return 3 +end +~~~~~~~ +. +
def foo(x)
+  return 3
+end
+
+```````````````````````````````` + + +```````````````````````````````` example +````; +```` +. +
+```````````````````````````````` + + +[Info strings] for backtick code blocks cannot contain backticks: + +```````````````````````````````` example +``` aa ``` +foo +. +

aa +foo

+```````````````````````````````` + + +[Info strings] for tilde code blocks can contain backticks and tildes: + +```````````````````````````````` example +~~~ aa ``` ~~~ +foo +~~~ +. +
foo
+
+```````````````````````````````` + + +Closing code fences cannot have [info strings]: + +```````````````````````````````` example +``` +``` aaa +``` +. +
``` aaa
+
+```````````````````````````````` + + + +## HTML blocks + +An [HTML block](@) is a group of lines that is treated +as raw HTML (and will not be escaped in HTML output). + +There are seven kinds of [HTML block], which can be defined by their +start and end conditions. The block begins with a line that meets a +[start condition](@) (after up to three spaces optional indentation). +It ends with the first subsequent line that meets a matching [end +condition](@), or the last line of the document, or the last line of +the [container block](#container-blocks) containing the current HTML +block, if no line is encountered that meets the [end condition]. If +the first line meets both the [start condition] and the [end +condition], the block will contain just that line. + +1. **Start condition:** line begins with the string ``, or the end of the line.\ +**End condition:** line contains an end tag +``, `
`, or `` (case-insensitive; it +need not match the start tag). + +2. **Start condition:** line begins with the string ``. + +3. **Start condition:** line begins with the string ``. + +4. **Start condition:** line begins with the string ``. + +5. **Start condition:** line begins with the string +``. + +6. **Start condition:** line begins the string `<` or ``, or +the string `/>`.\ +**End condition:** line is followed by a [blank line]. + +7. **Start condition:** line begins with a complete [open tag] +(with any [tag name] other than `script`, +`style`, or `pre`) or a complete [closing tag], +followed only by [whitespace] or the end of the line.\ +**End condition:** line is followed by a [blank line]. + +HTML blocks continue until they are closed by their appropriate +[end condition], or the last line of the document or other [container +block](#container-blocks). This means any HTML **within an HTML +block** that might otherwise be recognised as a start condition will +be ignored by the parser and passed through as-is, without changing +the parser's state. + +For instance, `
` within a HTML block started by `` will not affect
+the parser state; as the HTML block was started in by start condition 6, it
+will end at any blank line. This can be surprising:
+
+```````````````````````````````` example
+
+
+**Hello**,
+
+_world_.
+
+
+. +
+
+**Hello**,
+

world. +

+
+```````````````````````````````` + +In this case, the HTML block is terminated by the newline — the `**Hello**` +text remains verbatim — and regular parsing resumes, with a paragraph, +emphasised `world` and inline and block HTML following. + +All types of [HTML blocks] except type 7 may interrupt +a paragraph. Blocks of type 7 may not interrupt a paragraph. +(This restriction is intended to prevent unwanted interpretation +of long tags inside a wrapped paragraph as starting HTML blocks.) + +Some simple examples follow. Here are some basic HTML blocks +of type 6: + +```````````````````````````````` example + + + + +
+ hi +
+ +okay. +. + + + + +
+ hi +
+

okay.

+```````````````````````````````` + + +```````````````````````````````` example +
+*foo* +```````````````````````````````` + + +Here we have two HTML blocks with a Markdown paragraph between them: + +```````````````````````````````` example +
+ +*Markdown* + +
+. +
+

Markdown

+
+```````````````````````````````` + + +The tag on the first line can be partial, as long +as it is split where there would be whitespace: + +```````````````````````````````` example +
+
+. +
+
+```````````````````````````````` + + +```````````````````````````````` example +
+
+. +
+
+```````````````````````````````` + + +An open tag need not be closed: +```````````````````````````````` example +
+*foo* + +*bar* +. +
+*foo* +

bar

+```````````````````````````````` + + + +A partial tag need not even be completed (garbage +in, garbage out): + +```````````````````````````````` example +
+. + +```````````````````````````````` + + +```````````````````````````````` example +
+foo +
+. +
+foo +
+```````````````````````````````` + + +Everything until the next blank line or end of document +gets included in the HTML block. So, in the following +example, what looks like a Markdown code block +is actually part of the HTML block, which continues until a blank +line or the end of the document is reached: + +```````````````````````````````` example +
+``` c +int x = 33; +``` +. +
+``` c +int x = 33; +``` +```````````````````````````````` + + +To start an [HTML block] with a tag that is *not* in the +list of block-level tags in (6), you must put the tag by +itself on the first line (and it must be complete): + +```````````````````````````````` example + +*bar* + +. + +*bar* + +```````````````````````````````` + + +In type 7 blocks, the [tag name] can be anything: + +```````````````````````````````` example + +*bar* + +. + +*bar* + +```````````````````````````````` + + +```````````````````````````````` example + +*bar* + +. + +*bar* + +```````````````````````````````` + + +```````````````````````````````` example + +*bar* +. + +*bar* +```````````````````````````````` + + +These rules are designed to allow us to work with tags that +can function as either block-level or inline-level tags. +The `` tag is a nice example. We can surround content with +`` tags in three different ways. In this case, we get a raw +HTML block, because the `` tag is on a line by itself: + +```````````````````````````````` example + +*foo* + +. + +*foo* + +```````````````````````````````` + + +In this case, we get a raw HTML block that just includes +the `` tag (because it ends with the following blank +line). So the contents get interpreted as CommonMark: + +```````````````````````````````` example + + +*foo* + + +. + +

foo

+
+```````````````````````````````` + + +Finally, in this case, the `` tags are interpreted +as [raw HTML] *inside* the CommonMark paragraph. (Because +the tag is not on a line by itself, we get inline HTML +rather than an [HTML block].) + +```````````````````````````````` example +*foo* +. +

foo

+```````````````````````````````` + + +HTML tags designed to contain literal content +(`script`, `style`, `pre`), comments, processing instructions, +and declarations are treated somewhat differently. +Instead of ending at the first blank line, these blocks +end at the first line containing a corresponding end tag. +As a result, these blocks can contain blank lines: + +A pre tag (type 1): + +```````````````````````````````` example +

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+okay +. +

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+

okay

+```````````````````````````````` + + +A script tag (type 1): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + +A style tag (type 1): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + +If there is no matching end tag, the block will end at the +end of the document (or the enclosing [block quote][block quotes] +or [list item][list items]): + +```````````````````````````````` example + +*foo* +. + +

foo

+```````````````````````````````` + + +```````````````````````````````` example +*bar* +*baz* +. +*bar* +

baz

+```````````````````````````````` + + +Note that anything on the last line after the +end tag will be included in the [HTML block]: + +```````````````````````````````` example +1. *bar* +. +1. *bar* +```````````````````````````````` + + +A comment (type 2): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + + +A processing instruction (type 3): + +```````````````````````````````` example +'; + +?> +okay +. +'; + +?> +

okay

+```````````````````````````````` + + +A declaration (type 4): + +```````````````````````````````` example + +. + +```````````````````````````````` + + +CDATA (type 5): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + +The opening tag can be indented 1-3 spaces, but not 4: + +```````````````````````````````` example + + + +. + +
<!-- foo -->
+
+```````````````````````````````` + + +```````````````````````````````` example +
+ +
+. +
+
<div>
+
+```````````````````````````````` + + +An HTML block of types 1--6 can interrupt a paragraph, and need not be +preceded by a blank line. + +```````````````````````````````` example +Foo +
+bar +
+. +

Foo

+
+bar +
+```````````````````````````````` + + +However, a following blank line is needed, except at the end of +a document, and except for blocks of types 1--5, [above][HTML +block]: + +```````````````````````````````` example +
+bar +
+*foo* +. +
+bar +
+*foo* +```````````````````````````````` + + +HTML blocks of type 7 cannot interrupt a paragraph: + +```````````````````````````````` example +Foo + +baz +. +

Foo + +baz

+```````````````````````````````` + + +This rule differs from John Gruber's original Markdown syntax +specification, which says: + +> The only restrictions are that block-level HTML elements — +> e.g. `
`, ``, `
`, `

`, etc. — must be separated from +> surrounding content by blank lines, and the start and end tags of the +> block should not be indented with tabs or spaces. + +In some ways Gruber's rule is more restrictive than the one given +here: + +- It requires that an HTML block be preceded by a blank line. +- It does not allow the start tag to be indented. +- It requires a matching end tag, which it also does not allow to + be indented. + +Most Markdown implementations (including some of Gruber's own) do not +respect all of these restrictions. + +There is one respect, however, in which Gruber's rule is more liberal +than the one given here, since it allows blank lines to occur inside +an HTML block. There are two reasons for disallowing them here. +First, it removes the need to parse balanced tags, which is +expensive and can require backtracking from the end of the document +if no matching end tag is found. Second, it provides a very simple +and flexible way of including Markdown content inside HTML tags: +simply separate the Markdown from the HTML using blank lines: + +Compare: + +```````````````````````````````` example +

+ +*Emphasized* text. + +
+. +
+

Emphasized text.

+
+```````````````````````````````` + + +```````````````````````````````` example +
+*Emphasized* text. +
+. +
+*Emphasized* text. +
+```````````````````````````````` + + +Some Markdown implementations have adopted a convention of +interpreting content inside tags as text if the open tag has +the attribute `markdown=1`. The rule given above seems a simpler and +more elegant way of achieving the same expressive power, which is also +much simpler to parse. + +The main potential drawback is that one can no longer paste HTML +blocks into Markdown documents with 100% reliability. However, +*in most cases* this will work fine, because the blank lines in +HTML are usually followed by HTML block tags. For example: + +```````````````````````````````` example +
+ + + + + + + +
+Hi +
+. + + + + +
+Hi +
+```````````````````````````````` + + +There are problems, however, if the inner tags are indented +*and* separated by spaces, as then they will be interpreted as +an indented code block: + +```````````````````````````````` example + + + + + + + + +
+ Hi +
+. + + +
<td>
+  Hi
+</td>
+
+ +
+```````````````````````````````` + + +Fortunately, blank lines are usually not necessary and can be +deleted. The exception is inside `
` tags, but as described
+[above][HTML blocks], raw HTML blocks starting with `
`
+*can* contain blank lines.
+
+## Link reference definitions
+
+A [link reference definition](@)
+consists of a [link label], indented up to three spaces, followed
+by a colon (`:`), optional [whitespace] (including up to one
+[line ending]), a [link destination],
+optional [whitespace] (including up to one
+[line ending]), and an optional [link
+title], which if it is present must be separated
+from the [link destination] by [whitespace].
+No further [non-whitespace characters] may occur on the line.
+
+A [link reference definition]
+does not correspond to a structural element of a document.  Instead, it
+defines a label which can be used in [reference links]
+and reference-style [images] elsewhere in the document.  [Link
+reference definitions] can come either before or after the links that use
+them.
+
+```````````````````````````````` example
+[foo]: /url "title"
+
+[foo]
+.
+

foo

+```````````````````````````````` + + +```````````````````````````````` example + [foo]: + /url + 'the title' + +[foo] +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[Foo*bar\]]:my_(url) 'title (with parens)' + +[Foo*bar\]] +. +

Foo*bar]

+```````````````````````````````` + + +```````````````````````````````` example +[Foo bar]: + +'title' + +[Foo bar] +. +

Foo bar

+```````````````````````````````` + + +The title may extend over multiple lines: + +```````````````````````````````` example +[foo]: /url ' +title +line1 +line2 +' + +[foo] +. +

foo

+```````````````````````````````` + + +However, it may not contain a [blank line]: + +```````````````````````````````` example +[foo]: /url 'title + +with blank line' + +[foo] +. +

[foo]: /url 'title

+

with blank line'

+

[foo]

+```````````````````````````````` + + +The title may be omitted: + +```````````````````````````````` example +[foo]: +/url + +[foo] +. +

foo

+```````````````````````````````` + + +The link destination may not be omitted: + +```````````````````````````````` example +[foo]: + +[foo] +. +

[foo]:

+

[foo]

+```````````````````````````````` + + However, an empty link destination may be specified using + angle brackets: + +```````````````````````````````` example +[foo]: <> + +[foo] +. +

foo

+```````````````````````````````` + +The title must be separated from the link destination by +whitespace: + +```````````````````````````````` example +[foo]: (baz) + +[foo] +. +

[foo]: (baz)

+

[foo]

+```````````````````````````````` + + +Both title and destination can contain backslash escapes +and literal backslashes: + +```````````````````````````````` example +[foo]: /url\bar\*baz "foo\"bar\baz" + +[foo] +. +

foo

+```````````````````````````````` + + +A link can come before its corresponding definition: + +```````````````````````````````` example +[foo] + +[foo]: url +. +

foo

+```````````````````````````````` + + +If there are several matching definitions, the first one takes +precedence: + +```````````````````````````````` example +[foo] + +[foo]: first +[foo]: second +. +

foo

+```````````````````````````````` + + +As noted in the section on [Links], matching of labels is +case-insensitive (see [matches]). + +```````````````````````````````` example +[FOO]: /url + +[Foo] +. +

Foo

+```````````````````````````````` + + +```````````````````````````````` example +[ΑΓΩ]: /φου + +[αγω] +. +

αγω

+```````````````````````````````` + + +Here is a link reference definition with no corresponding link. +It contributes nothing to the document. + +```````````````````````````````` example +[foo]: /url +. +```````````````````````````````` + + +Here is another one: + +```````````````````````````````` example +[ +foo +]: /url +bar +. +

bar

+```````````````````````````````` + + +This is not a link reference definition, because there are +[non-whitespace characters] after the title: + +```````````````````````````````` example +[foo]: /url "title" ok +. +

[foo]: /url "title" ok

+```````````````````````````````` + + +This is a link reference definition, but it has no title: + +```````````````````````````````` example +[foo]: /url +"title" ok +. +

"title" ok

+```````````````````````````````` + + +This is not a link reference definition, because it is indented +four spaces: + +```````````````````````````````` example + [foo]: /url "title" + +[foo] +. +
[foo]: /url "title"
+
+

[foo]

+```````````````````````````````` + + +This is not a link reference definition, because it occurs inside +a code block: + +```````````````````````````````` example +``` +[foo]: /url +``` + +[foo] +. +
[foo]: /url
+
+

[foo]

+```````````````````````````````` + + +A [link reference definition] cannot interrupt a paragraph. + +```````````````````````````````` example +Foo +[bar]: /baz + +[bar] +. +

Foo +[bar]: /baz

+

[bar]

+```````````````````````````````` + + +However, it can directly follow other block elements, such as headings +and thematic breaks, and it need not be followed by a blank line. + +```````````````````````````````` example +# [Foo] +[foo]: /url +> bar +. +

Foo

+
+

bar

+
+```````````````````````````````` + +```````````````````````````````` example +[foo]: /url +bar +=== +[foo] +. +

bar

+

foo

+```````````````````````````````` + +```````````````````````````````` example +[foo]: /url +=== +[foo] +. +

=== +foo

+```````````````````````````````` + + +Several [link reference definitions] +can occur one after another, without intervening blank lines. + +```````````````````````````````` example +[foo]: /foo-url "foo" +[bar]: /bar-url + "bar" +[baz]: /baz-url + +[foo], +[bar], +[baz] +. +

foo, +bar, +baz

+```````````````````````````````` + + +[Link reference definitions] can occur +inside block containers, like lists and block quotations. They +affect the entire document, not just the container in which they +are defined: + +```````````````````````````````` example +[foo] + +> [foo]: /url +. +

foo

+
+
+```````````````````````````````` + + +Whether something is a [link reference definition] is +independent of whether the link reference it defines is +used in the document. Thus, for example, the following +document contains just a link reference definition, and +no visible content: + +```````````````````````````````` example +[foo]: /url +. +```````````````````````````````` + + +## Paragraphs + +A sequence of non-blank lines that cannot be interpreted as other +kinds of blocks forms a [paragraph](@). +The contents of the paragraph are the result of parsing the +paragraph's raw content as inlines. The paragraph's raw content +is formed by concatenating the lines and removing initial and final +[whitespace]. + +A simple example with two paragraphs: + +```````````````````````````````` example +aaa + +bbb +. +

aaa

+

bbb

+```````````````````````````````` + + +Paragraphs can contain multiple lines, but no blank lines: + +```````````````````````````````` example +aaa +bbb + +ccc +ddd +. +

aaa +bbb

+

ccc +ddd

+```````````````````````````````` + + +Multiple blank lines between paragraph have no effect: + +```````````````````````````````` example +aaa + + +bbb +. +

aaa

+

bbb

+```````````````````````````````` + + +Leading spaces are skipped: + +```````````````````````````````` example + aaa + bbb +. +

aaa +bbb

+```````````````````````````````` + + +Lines after the first may be indented any amount, since indented +code blocks cannot interrupt paragraphs. + +```````````````````````````````` example +aaa + bbb + ccc +. +

aaa +bbb +ccc

+```````````````````````````````` + + +However, the first line may be indented at most three spaces, +or an indented code block will be triggered: + +```````````````````````````````` example + aaa +bbb +. +

aaa +bbb

+```````````````````````````````` + + +```````````````````````````````` example + aaa +bbb +. +
aaa
+
+

bbb

+```````````````````````````````` + + +Final spaces are stripped before inline parsing, so a paragraph +that ends with two or more spaces will not end with a [hard line +break]: + +```````````````````````````````` example +aaa +bbb +. +

aaa
+bbb

+```````````````````````````````` + + +## Blank lines + +[Blank lines] between block-level elements are ignored, +except for the role they play in determining whether a [list] +is [tight] or [loose]. + +Blank lines at the beginning and end of the document are also ignored. + +```````````````````````````````` example + + +aaa + + +# aaa + + +. +

aaa

+

aaa

+```````````````````````````````` + + + +# Container blocks + +A [container block](#container-blocks) is a block that has other +blocks as its contents. There are two basic kinds of container blocks: +[block quotes] and [list items]. +[Lists] are meta-containers for [list items]. + +We define the syntax for container blocks recursively. The general +form of the definition is: + +> If X is a sequence of blocks, then the result of +> transforming X in such-and-such a way is a container of type Y +> with these blocks as its content. + +So, we explain what counts as a block quote or list item by explaining +how these can be *generated* from their contents. This should suffice +to define the syntax, although it does not give a recipe for *parsing* +these constructions. (A recipe is provided below in the section entitled +[A parsing strategy](#appendix-a-parsing-strategy).) + +## Block quotes + +A [block quote marker](@) +consists of 0-3 spaces of initial indent, plus (a) the character `>` together +with a following space, or (b) a single character `>` not followed by a space. + +The following rules define [block quotes]: + +1. **Basic case.** If a string of lines *Ls* constitute a sequence + of blocks *Bs*, then the result of prepending a [block quote + marker] to the beginning of each line in *Ls* + is a [block quote](#block-quotes) containing *Bs*. + +2. **Laziness.** If a string of lines *Ls* constitute a [block + quote](#block-quotes) with contents *Bs*, then the result of deleting + the initial [block quote marker] from one or + more lines in which the next [non-whitespace character] after the [block + quote marker] is [paragraph continuation + text] is a block quote with *Bs* as its content. + [Paragraph continuation text](@) is text + that will be parsed as part of the content of a paragraph, but does + not occur at the beginning of the paragraph. + +3. **Consecutiveness.** A document cannot contain two [block + quotes] in a row unless there is a [blank line] between them. + +Nothing else counts as a [block quote](#block-quotes). + +Here is a simple example: + +```````````````````````````````` example +> # Foo +> bar +> baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +The spaces after the `>` characters can be omitted: + +```````````````````````````````` example +># Foo +>bar +> baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +The `>` characters can be indented 1-3 spaces: + +```````````````````````````````` example + > # Foo + > bar + > baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +Four spaces gives us a code block: + +```````````````````````````````` example + > # Foo + > bar + > baz +. +
> # Foo
+> bar
+> baz
+
+```````````````````````````````` + + +The Laziness clause allows us to omit the `>` before +[paragraph continuation text]: + +```````````````````````````````` example +> # Foo +> bar +baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +A block quote can contain some lazy and some non-lazy +continuation lines: + +```````````````````````````````` example +> bar +baz +> foo +. +
+

bar +baz +foo

+
+```````````````````````````````` + + +Laziness only applies to lines that would have been continuations of +paragraphs had they been prepended with [block quote markers]. +For example, the `> ` cannot be omitted in the second line of + +``` markdown +> foo +> --- +``` + +without changing the meaning: + +```````````````````````````````` example +> foo +--- +. +
+

foo

+
+
+```````````````````````````````` + + +Similarly, if we omit the `> ` in the second line of + +``` markdown +> - foo +> - bar +``` + +then the block quote ends after the first line: + +```````````````````````````````` example +> - foo +- bar +. +
+
    +
  • foo
  • +
+
+
    +
  • bar
  • +
+```````````````````````````````` + + +For the same reason, we can't omit the `> ` in front of +subsequent lines of an indented or fenced code block: + +```````````````````````````````` example +> foo + bar +. +
+
foo
+
+
+
bar
+
+```````````````````````````````` + + +```````````````````````````````` example +> ``` +foo +``` +. +
+
+
+

foo

+
+```````````````````````````````` + + +Note that in the following case, we have a [lazy +continuation line]: + +```````````````````````````````` example +> foo + - bar +. +
+

foo +- bar

+
+```````````````````````````````` + + +To see why, note that in + +```markdown +> foo +> - bar +``` + +the `- bar` is indented too far to start a list, and can't +be an indented code block because indented code blocks cannot +interrupt paragraphs, so it is [paragraph continuation text]. + +A block quote can be empty: + +```````````````````````````````` example +> +. +
+
+```````````````````````````````` + + +```````````````````````````````` example +> +> +> +. +
+
+```````````````````````````````` + + +A block quote can have initial or final blank lines: + +```````````````````````````````` example +> +> foo +> +. +
+

foo

+
+```````````````````````````````` + + +A blank line always separates block quotes: + +```````````````````````````````` example +> foo + +> bar +. +
+

foo

+
+
+

bar

+
+```````````````````````````````` + + +(Most current Markdown implementations, including John Gruber's +original `Markdown.pl`, will parse this example as a single block quote +with two paragraphs. But it seems better to allow the author to decide +whether two block quotes or one are wanted.) + +Consecutiveness means that if we put these block quotes together, +we get a single block quote: + +```````````````````````````````` example +> foo +> bar +. +
+

foo +bar

+
+```````````````````````````````` + + +To get a block quote with two paragraphs, use: + +```````````````````````````````` example +> foo +> +> bar +. +
+

foo

+

bar

+
+```````````````````````````````` + + +Block quotes can interrupt paragraphs: + +```````````````````````````````` example +foo +> bar +. +

foo

+
+

bar

+
+```````````````````````````````` + + +In general, blank lines are not needed before or after block +quotes: + +```````````````````````````````` example +> aaa +*** +> bbb +. +
+

aaa

+
+
+
+

bbb

+
+```````````````````````````````` + + +However, because of laziness, a blank line is needed between +a block quote and a following paragraph: + +```````````````````````````````` example +> bar +baz +. +
+

bar +baz

+
+```````````````````````````````` + + +```````````````````````````````` example +> bar + +baz +. +
+

bar

+
+

baz

+```````````````````````````````` + + +```````````````````````````````` example +> bar +> +baz +. +
+

bar

+
+

baz

+```````````````````````````````` + + +It is a consequence of the Laziness rule that any number +of initial `>`s may be omitted on a continuation line of a +nested block quote: + +```````````````````````````````` example +> > > foo +bar +. +
+
+
+

foo +bar

+
+
+
+```````````````````````````````` + + +```````````````````````````````` example +>>> foo +> bar +>>baz +. +
+
+
+

foo +bar +baz

+
+
+
+```````````````````````````````` + + +When including an indented code block in a block quote, +remember that the [block quote marker] includes +both the `>` and a following space. So *five spaces* are needed after +the `>`: + +```````````````````````````````` example +> code + +> not code +. +
+
code
+
+
+
+

not code

+
+```````````````````````````````` + + + +## List items + +A [list marker](@) is a +[bullet list marker] or an [ordered list marker]. + +A [bullet list marker](@) +is a `-`, `+`, or `*` character. + +An [ordered list marker](@) +is a sequence of 1--9 arabic digits (`0-9`), followed by either a +`.` character or a `)` character. (The reason for the length +limit is that with 10 digits we start seeing integer overflows +in some browsers.) + +The following rules define [list items]: + +1. **Basic case.** If a sequence of lines *Ls* constitute a sequence of + blocks *Bs* starting with a [non-whitespace character], and *M* is a + list marker of width *W* followed by 1 ≤ *N* ≤ 4 spaces, then the result + of prepending *M* and the following spaces to the first line of + *Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a + list item with *Bs* as its contents. The type of the list item + (bullet or ordered) is determined by the type of its list marker. + If the list item is ordered, then it is also assigned a start + number, based on the ordered list marker. + + Exceptions: + + 1. When the first list item in a [list] interrupts + a paragraph---that is, when it starts on a line that would + otherwise count as [paragraph continuation text]---then (a) + the lines *Ls* must not begin with a blank line, and (b) if + the list item is ordered, the start number must be 1. + 2. If any line is a [thematic break][thematic breaks] then + that line is not a list item. + +For example, let *Ls* be the lines + +```````````````````````````````` example +A paragraph +with two lines. + + indented code + +> A block quote. +. +

A paragraph +with two lines.

+
indented code
+
+
+

A block quote.

+
+```````````````````````````````` + + +And let *M* be the marker `1.`, and *N* = 2. Then rule #1 says +that the following is an ordered list item with start number 1, +and the same contents as *Ls*: + +```````````````````````````````` example +1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +The most important thing to notice is that the position of +the text after the list marker determines how much indentation +is needed in subsequent blocks in the list item. If the list +marker takes up two spaces, and there are three spaces between +the list marker and the next [non-whitespace character], then blocks +must be indented five spaces in order to fall under the list +item. + +Here are some examples showing how far content must be indented to be +put under the list item: + +```````````````````````````````` example +- one + + two +. +
    +
  • one
  • +
+

two

+```````````````````````````````` + + +```````````````````````````````` example +- one + + two +. +
    +
  • +

    one

    +

    two

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example + - one + + two +. +
    +
  • one
  • +
+
 two
+
+```````````````````````````````` + + +```````````````````````````````` example + - one + + two +. +
    +
  • +

    one

    +

    two

    +
  • +
+```````````````````````````````` + + +It is tempting to think of this in terms of columns: the continuation +blocks must be indented at least to the column of the first +[non-whitespace character] after the list marker. However, that is not quite right. +The spaces after the list marker determine how much relative indentation +is needed. Which column this indentation reaches will depend on +how the list item is embedded in other constructions, as shown by +this example: + +```````````````````````````````` example + > > 1. one +>> +>> two +. +
+
+
    +
  1. +

    one

    +

    two

    +
  2. +
+
+
+```````````````````````````````` + + +Here `two` occurs in the same column as the list marker `1.`, +but is actually contained in the list item, because there is +sufficient indentation after the last containing blockquote marker. + +The converse is also possible. In the following example, the word `two` +occurs far to the right of the initial text of the list item, `one`, but +it is not considered part of the list item, because it is not indented +far enough past the blockquote marker: + +```````````````````````````````` example +>>- one +>> + > > two +. +
+
+
    +
  • one
  • +
+

two

+
+
+```````````````````````````````` + + +Note that at least one space is needed between the list marker and +any following content, so these are not list items: + +```````````````````````````````` example +-one + +2.two +. +

-one

+

2.two

+```````````````````````````````` + + +A list item may contain blocks that are separated by more than +one blank line. + +```````````````````````````````` example +- foo + + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + + +A list item may contain any kind of block: + +```````````````````````````````` example +1. foo + + ``` + bar + ``` + + baz + + > bam +. +
    +
  1. +

    foo

    +
    bar
    +
    +

    baz

    +
    +

    bam

    +
    +
  2. +
+```````````````````````````````` + + +A list item that contains an indented code block will preserve +empty lines within the code block verbatim. + +```````````````````````````````` example +- Foo + + bar + + + baz +. +
    +
  • +

    Foo

    +
    bar
    +
    +
    +baz
    +
    +
  • +
+```````````````````````````````` + +Note that ordered list start numbers must be nine digits or less: + +```````````````````````````````` example +123456789. ok +. +
    +
  1. ok
  2. +
+```````````````````````````````` + + +```````````````````````````````` example +1234567890. not ok +. +

1234567890. not ok

+```````````````````````````````` + + +A start number may begin with 0s: + +```````````````````````````````` example +0. ok +. +
    +
  1. ok
  2. +
+```````````````````````````````` + + +```````````````````````````````` example +003. ok +. +
    +
  1. ok
  2. +
+```````````````````````````````` + + +A start number may not be negative: + +```````````````````````````````` example +-1. not ok +. +

-1. not ok

+```````````````````````````````` + + + +2. **Item starting with indented code.** If a sequence of lines *Ls* + constitute a sequence of blocks *Bs* starting with an indented code + block, and *M* is a list marker of width *W* followed by + one space, then the result of prepending *M* and the following + space to the first line of *Ls*, and indenting subsequent lines of + *Ls* by *W + 1* spaces, is a list item with *Bs* as its contents. + If a line is empty, then it need not be indented. The type of the + list item (bullet or ordered) is determined by the type of its list + marker. If the list item is ordered, then it is also assigned a + start number, based on the ordered list marker. + +An indented code block will have to be indented four spaces beyond +the edge of the region where text will be included in the list item. +In the following case that is 6 spaces: + +```````````````````````````````` example +- foo + + bar +. +
    +
  • +

    foo

    +
    bar
    +
    +
  • +
+```````````````````````````````` + + +And in this case it is 11 spaces: + +```````````````````````````````` example + 10. foo + + bar +. +
    +
  1. +

    foo

    +
    bar
    +
    +
  2. +
+```````````````````````````````` + + +If the *first* block in the list item is an indented code block, +then by rule #2, the contents must be indented *one* space after the +list marker: + +```````````````````````````````` example + indented code + +paragraph + + more code +. +
indented code
+
+

paragraph

+
more code
+
+```````````````````````````````` + + +```````````````````````````````` example +1. indented code + + paragraph + + more code +. +
    +
  1. +
    indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
+```````````````````````````````` + + +Note that an additional space indent is interpreted as space +inside the code block: + +```````````````````````````````` example +1. indented code + + paragraph + + more code +. +
    +
  1. +
     indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
+```````````````````````````````` + + +Note that rules #1 and #2 only apply to two cases: (a) cases +in which the lines to be included in a list item begin with a +[non-whitespace character], and (b) cases in which +they begin with an indented code +block. In a case like the following, where the first block begins with +a three-space indent, the rules do not allow us to form a list item by +indenting the whole thing and prepending a list marker: + +```````````````````````````````` example + foo + +bar +. +

foo

+

bar

+```````````````````````````````` + + +```````````````````````````````` example +- foo + + bar +. +
    +
  • foo
  • +
+

bar

+```````````````````````````````` + + +This is not a significant restriction, because when a block begins +with 1-3 spaces indent, the indentation can always be removed without +a change in interpretation, allowing rule #1 to be applied. So, in +the above case: + +```````````````````````````````` example +- foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + + +3. **Item starting with a blank line.** If a sequence of lines *Ls* + starting with a single [blank line] constitute a (possibly empty) + sequence of blocks *Bs*, not separated from each other by more than + one blank line, and *M* is a list marker of width *W*, + then the result of prepending *M* to the first line of *Ls*, and + indenting subsequent lines of *Ls* by *W + 1* spaces, is a list + item with *Bs* as its contents. + If a line is empty, then it need not be indented. The type of the + list item (bullet or ordered) is determined by the type of its list + marker. If the list item is ordered, then it is also assigned a + start number, based on the ordered list marker. + +Here are some list items that start with a blank line but are not empty: + +```````````````````````````````` example +- + foo +- + ``` + bar + ``` +- + baz +. +
    +
  • foo
  • +
  • +
    bar
    +
    +
  • +
  • +
    baz
    +
    +
  • +
+```````````````````````````````` + +When the list item starts with a blank line, the number of spaces +following the list marker doesn't change the required indentation: + +```````````````````````````````` example +- + foo +. +
    +
  • foo
  • +
+```````````````````````````````` + + +A list item can begin with at most one blank line. +In the following example, `foo` is not part of the list +item: + +```````````````````````````````` example +- + + foo +. +
    +
  • +
+

foo

+```````````````````````````````` + + +Here is an empty bullet list item: + +```````````````````````````````` example +- foo +- +- bar +. +
    +
  • foo
  • +
  • +
  • bar
  • +
+```````````````````````````````` + + +It does not matter whether there are spaces following the [list marker]: + +```````````````````````````````` example +- foo +- +- bar +. +
    +
  • foo
  • +
  • +
  • bar
  • +
+```````````````````````````````` + + +Here is an empty ordered list item: + +```````````````````````````````` example +1. foo +2. +3. bar +. +
    +
  1. foo
  2. +
  3. +
  4. bar
  5. +
+```````````````````````````````` + + +A list may start or end with an empty list item: + +```````````````````````````````` example +* +. +
    +
  • +
+```````````````````````````````` + +However, an empty list item cannot interrupt a paragraph: + +```````````````````````````````` example +foo +* + +foo +1. +. +

foo +*

+

foo +1.

+```````````````````````````````` + + +4. **Indentation.** If a sequence of lines *Ls* constitutes a list item + according to rule #1, #2, or #3, then the result of indenting each line + of *Ls* by 1-3 spaces (the same for each line) also constitutes a + list item with the same contents and attributes. If a line is + empty, then it need not be indented. + +Indented one space: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Indented two spaces: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Indented three spaces: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Four spaces indent gives a code block: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
1.  A paragraph
+    with two lines.
+
+        indented code
+
+    > A block quote.
+
+```````````````````````````````` + + + +5. **Laziness.** If a string of lines *Ls* constitute a [list + item](#list-items) with contents *Bs*, then the result of deleting + some or all of the indentation from one or more lines in which the + next [non-whitespace character] after the indentation is + [paragraph continuation text] is a + list item with the same contents and attributes. The unindented + lines are called + [lazy continuation line](@)s. + +Here is an example with [lazy continuation lines]: + +```````````````````````````````` example + 1. A paragraph +with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Indentation can be partially deleted: + +```````````````````````````````` example + 1. A paragraph + with two lines. +. +
    +
  1. A paragraph +with two lines.
  2. +
+```````````````````````````````` + + +These examples show how laziness can work in nested structures: + +```````````````````````````````` example +> 1. > Blockquote +continued here. +. +
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
+```````````````````````````````` + + +```````````````````````````````` example +> 1. > Blockquote +> continued here. +. +
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
+```````````````````````````````` + + + +6. **That's all.** Nothing that is not counted as a list item by rules + #1--5 counts as a [list item](#list-items). + +The rules for sublists follow from the general rules +[above][List items]. A sublist must be indented the same number +of spaces a paragraph would need to be in order to be included +in the list item. + +So, in this case we need two spaces indent: + +```````````````````````````````` example +- foo + - bar + - baz + - boo +. +
    +
  • foo +
      +
    • bar +
        +
      • baz +
          +
        • boo
        • +
        +
      • +
      +
    • +
    +
  • +
+```````````````````````````````` + + +One is not enough: + +```````````````````````````````` example +- foo + - bar + - baz + - boo +. +
    +
  • foo
  • +
  • bar
  • +
  • baz
  • +
  • boo
  • +
+```````````````````````````````` + + +Here we need four, because the list marker is wider: + +```````````````````````````````` example +10) foo + - bar +. +
    +
  1. foo +
      +
    • bar
    • +
    +
  2. +
+```````````````````````````````` + + +Three is not enough: + +```````````````````````````````` example +10) foo + - bar +. +
    +
  1. foo
  2. +
+
    +
  • bar
  • +
+```````````````````````````````` + + +A list may be the first block in a list item: + +```````````````````````````````` example +- - foo +. +
    +
  • +
      +
    • foo
    • +
    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. - 2. foo +. +
    +
  1. +
      +
    • +
        +
      1. foo
      2. +
      +
    • +
    +
  2. +
+```````````````````````````````` + + +A list item can contain a heading: + +```````````````````````````````` example +- # Foo +- Bar + --- + baz +. +
    +
  • +

    Foo

    +
  • +
  • +

    Bar

    +baz
  • +
+```````````````````````````````` + + +### Motivation + +John Gruber's Markdown spec says the following about list items: + +1. "List markers typically start at the left margin, but may be indented + by up to three spaces. List markers must be followed by one or more + spaces or a tab." + +2. "To make lists look nice, you can wrap items with hanging indents.... + But if you don't want to, you don't have to." + +3. "List items may consist of multiple paragraphs. Each subsequent + paragraph in a list item must be indented by either 4 spaces or one + tab." + +4. "It looks nice if you indent every line of the subsequent paragraphs, + but here again, Markdown will allow you to be lazy." + +5. "To put a blockquote within a list item, the blockquote's `>` + delimiters need to be indented." + +6. "To put a code block within a list item, the code block needs to be + indented twice — 8 spaces or two tabs." + +These rules specify that a paragraph under a list item must be indented +four spaces (presumably, from the left margin, rather than the start of +the list marker, but this is not said), and that code under a list item +must be indented eight spaces instead of the usual four. They also say +that a block quote must be indented, but not by how much; however, the +example given has four spaces indentation. Although nothing is said +about other kinds of block-level content, it is certainly reasonable to +infer that *all* block elements under a list item, including other +lists, must be indented four spaces. This principle has been called the +*four-space rule*. + +The four-space rule is clear and principled, and if the reference +implementation `Markdown.pl` had followed it, it probably would have +become the standard. However, `Markdown.pl` allowed paragraphs and +sublists to start with only two spaces indentation, at least on the +outer level. Worse, its behavior was inconsistent: a sublist of an +outer-level list needed two spaces indentation, but a sublist of this +sublist needed three spaces. It is not surprising, then, that different +implementations of Markdown have developed very different rules for +determining what comes under a list item. (Pandoc and python-Markdown, +for example, stuck with Gruber's syntax description and the four-space +rule, while discount, redcarpet, marked, PHP Markdown, and others +followed `Markdown.pl`'s behavior more closely.) + +Unfortunately, given the divergences between implementations, there +is no way to give a spec for list items that will be guaranteed not +to break any existing documents. However, the spec given here should +correctly handle lists formatted with either the four-space rule or +the more forgiving `Markdown.pl` behavior, provided they are laid out +in a way that is natural for a human to read. + +The strategy here is to let the width and indentation of the list marker +determine the indentation necessary for blocks to fall under the list +item, rather than having a fixed and arbitrary number. The writer can +think of the body of the list item as a unit which gets indented to the +right enough to fit the list marker (and any indentation on the list +marker). (The laziness rule, #5, then allows continuation lines to be +unindented if needed.) + +This rule is superior, we claim, to any rule requiring a fixed level of +indentation from the margin. The four-space rule is clear but +unnatural. It is quite unintuitive that + +``` markdown +- foo + + bar + + - baz +``` + +should be parsed as two lists with an intervening paragraph, + +``` html +
    +
  • foo
  • +
+

bar

+
    +
  • baz
  • +
+``` + +as the four-space rule demands, rather than a single list, + +``` html +
    +
  • +

    foo

    +

    bar

    +
      +
    • baz
    • +
    +
  • +
+``` + +The choice of four spaces is arbitrary. It can be learned, but it is +not likely to be guessed, and it trips up beginners regularly. + +Would it help to adopt a two-space rule? The problem is that such +a rule, together with the rule allowing 1--3 spaces indentation of the +initial list marker, allows text that is indented *less than* the +original list marker to be included in the list item. For example, +`Markdown.pl` parses + +``` markdown + - one + + two +``` + +as a single list item, with `two` a continuation paragraph: + +``` html +
    +
  • +

    one

    +

    two

    +
  • +
+``` + +and similarly + +``` markdown +> - one +> +> two +``` + +as + +``` html +
+
    +
  • +

    one

    +

    two

    +
  • +
+
+``` + +This is extremely unintuitive. + +Rather than requiring a fixed indent from the margin, we could require +a fixed indent (say, two spaces, or even one space) from the list marker (which +may itself be indented). This proposal would remove the last anomaly +discussed. Unlike the spec presented above, it would count the following +as a list item with a subparagraph, even though the paragraph `bar` +is not indented as far as the first paragraph `foo`: + +``` markdown + 10. foo + + bar +``` + +Arguably this text does read like a list item with `bar` as a subparagraph, +which may count in favor of the proposal. However, on this proposal indented +code would have to be indented six spaces after the list marker. And this +would break a lot of existing Markdown, which has the pattern: + +``` markdown +1. foo + + indented code +``` + +where the code is indented eight spaces. The spec above, by contrast, will +parse this text as expected, since the code block's indentation is measured +from the beginning of `foo`. + +The one case that needs special treatment is a list item that *starts* +with indented code. How much indentation is required in that case, since +we don't have a "first paragraph" to measure from? Rule #2 simply stipulates +that in such cases, we require one space indentation from the list marker +(and then the normal four spaces for the indented code). This will match the +four-space rule in cases where the list marker plus its initial indentation +takes four spaces (a common case), but diverge in other cases. + +## Lists + +A [list](@) is a sequence of one or more +list items [of the same type]. The list items +may be separated by any number of blank lines. + +Two list items are [of the same type](@) +if they begin with a [list marker] of the same type. +Two list markers are of the +same type if (a) they are bullet list markers using the same character +(`-`, `+`, or `*`) or (b) they are ordered list numbers with the same +delimiter (either `.` or `)`). + +A list is an [ordered list](@) +if its constituent list items begin with +[ordered list markers], and a +[bullet list](@) if its constituent list +items begin with [bullet list markers]. + +The [start number](@) +of an [ordered list] is determined by the list number of +its initial list item. The numbers of subsequent list items are +disregarded. + +A list is [loose](@) if any of its constituent +list items are separated by blank lines, or if any of its constituent +list items directly contain two block-level elements with a blank line +between them. Otherwise a list is [tight](@). +(The difference in HTML output is that paragraphs in a loose list are +wrapped in `

` tags, while paragraphs in a tight list are not.) + +Changing the bullet or ordered list delimiter starts a new list: + +```````````````````````````````` example +- foo +- bar ++ baz +. +

    +
  • foo
  • +
  • bar
  • +
+
    +
  • baz
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. foo +2. bar +3) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
+
    +
  1. baz
  2. +
+```````````````````````````````` + + +In CommonMark, a list can interrupt a paragraph. That is, +no blank line is needed to separate a paragraph from a following +list: + +```````````````````````````````` example +Foo +- bar +- baz +. +

Foo

+
    +
  • bar
  • +
  • baz
  • +
+```````````````````````````````` + +`Markdown.pl` does not allow this, through fear of triggering a list +via a numeral in a hard-wrapped line: + +``` markdown +The number of windows in my house is +14. The number of doors is 6. +``` + +Oddly, though, `Markdown.pl` *does* allow a blockquote to +interrupt a paragraph, even though the same considerations might +apply. + +In CommonMark, we do allow lists to interrupt paragraphs, for +two reasons. First, it is natural and not uncommon for people +to start lists without blank lines: + +``` markdown +I need to buy +- new shoes +- a coat +- a plane ticket +``` + +Second, we are attracted to a + +> [principle of uniformity](@): +> if a chunk of text has a certain +> meaning, it will continue to have the same meaning when put into a +> container block (such as a list item or blockquote). + +(Indeed, the spec for [list items] and [block quotes] presupposes +this principle.) This principle implies that if + +``` markdown + * I need to buy + - new shoes + - a coat + - a plane ticket +``` + +is a list item containing a paragraph followed by a nested sublist, +as all Markdown implementations agree it is (though the paragraph +may be rendered without `

` tags, since the list is "tight"), +then + +``` markdown +I need to buy +- new shoes +- a coat +- a plane ticket +``` + +by itself should be a paragraph followed by a nested sublist. + +Since it is well established Markdown practice to allow lists to +interrupt paragraphs inside list items, the [principle of +uniformity] requires us to allow this outside list items as +well. ([reStructuredText](http://docutils.sourceforge.net/rst.html) +takes a different approach, requiring blank lines before lists +even inside other list items.) + +In order to solve of unwanted lists in paragraphs with +hard-wrapped numerals, we allow only lists starting with `1` to +interrupt paragraphs. Thus, + +```````````````````````````````` example +The number of windows in my house is +14. The number of doors is 6. +. +

The number of windows in my house is +14. The number of doors is 6.

+```````````````````````````````` + +We may still get an unintended result in cases like + +```````````````````````````````` example +The number of windows in my house is +1. The number of doors is 6. +. +

The number of windows in my house is

+
    +
  1. The number of doors is 6.
  2. +
+```````````````````````````````` + +but this rule should prevent most spurious list captures. + +There can be any number of blank lines between items: + +```````````````````````````````` example +- foo + +- bar + + +- baz +. +
    +
  • +

    foo

    +
  • +
  • +

    bar

    +
  • +
  • +

    baz

    +
  • +
+```````````````````````````````` + +```````````````````````````````` example +- foo + - bar + - baz + + + bim +. +
    +
  • foo +
      +
    • bar +
        +
      • +

        baz

        +

        bim

        +
      • +
      +
    • +
    +
  • +
+```````````````````````````````` + + +To separate consecutive lists of the same type, or to separate a +list from an indented code block that would otherwise be parsed +as a subparagraph of the final list item, you can insert a blank HTML +comment: + +```````````````````````````````` example +- foo +- bar + + + +- baz +- bim +. +
    +
  • foo
  • +
  • bar
  • +
+ +
    +
  • baz
  • +
  • bim
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- foo + + notcode + +- foo + + + + code +. +
    +
  • +

    foo

    +

    notcode

    +
  • +
  • +

    foo

    +
  • +
+ +
code
+
+```````````````````````````````` + + +List items need not be indented to the same level. The following +list items will be treated as items at the same list level, +since none is indented enough to belong to the previous list +item: + +```````````````````````````````` example +- a + - b + - c + - d + - e + - f +- g +. +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d
  • +
  • e
  • +
  • f
  • +
  • g
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. a + + 2. b + + 3. c +. +
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
  5. +

    c

    +
  6. +
+```````````````````````````````` + +Note, however, that list items may not be indented more than +three spaces. Here `- e` is treated as a paragraph continuation +line, because it is indented more than three spaces: + +```````````````````````````````` example +- a + - b + - c + - d + - e +. +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d +- e
  • +
+```````````````````````````````` + +And here, `3. c` is treated as in indented code block, +because it is indented four spaces and preceded by a +blank line. + +```````````````````````````````` example +1. a + + 2. b + + 3. c +. +
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
+
3. c
+
+```````````````````````````````` + + +This is a loose list, because there is a blank line between +two of the list items: + +```````````````````````````````` example +- a +- b + +- c +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    c

    +
  • +
+```````````````````````````````` + + +So is this, with a empty second item: + +```````````````````````````````` example +* a +* + +* c +. +
    +
  • +

    a

    +
  • +
  • +
  • +

    c

    +
  • +
+```````````````````````````````` + + +These are loose lists, even though there is no space between the items, +because one of the items directly contains two block-level elements +with a blank line between them: + +```````````````````````````````` example +- a +- b + + c +- d +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +

    c

    +
  • +
  • +

    d

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- a +- b + + [ref]: /url +- d +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    d

    +
  • +
+```````````````````````````````` + + +This is a tight list, because the blank lines are in a code block: + +```````````````````````````````` example +- a +- ``` + b + + + ``` +- c +. +
    +
  • a
  • +
  • +
    b
    +
    +
    +
    +
  • +
  • c
  • +
+```````````````````````````````` + + +This is a tight list, because the blank line is between two +paragraphs of a sublist. So the sublist is loose while +the outer list is tight: + +```````````````````````````````` example +- a + - b + + c +- d +. +
    +
  • a +
      +
    • +

      b

      +

      c

      +
    • +
    +
  • +
  • d
  • +
+```````````````````````````````` + + +This is a tight list, because the blank line is inside the +block quote: + +```````````````````````````````` example +* a + > b + > +* c +. +
    +
  • a +
    +

    b

    +
    +
  • +
  • c
  • +
+```````````````````````````````` + + +This list is tight, because the consecutive block elements +are not separated by blank lines: + +```````````````````````````````` example +- a + > b + ``` + c + ``` +- d +. +
    +
  • a +
    +

    b

    +
    +
    c
    +
    +
  • +
  • d
  • +
+```````````````````````````````` + + +A single-paragraph list is tight: + +```````````````````````````````` example +- a +. +
    +
  • a
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- a + - b +. +
    +
  • a +
      +
    • b
    • +
    +
  • +
+```````````````````````````````` + + +This list is loose, because of the blank line between the +two block elements in the list item: + +```````````````````````````````` example +1. ``` + foo + ``` + + bar +. +
    +
  1. +
    foo
    +
    +

    bar

    +
  2. +
+```````````````````````````````` + + +Here the outer list is loose, the inner list tight: + +```````````````````````````````` example +* foo + * bar + + baz +. +
    +
  • +

    foo

    +
      +
    • bar
    • +
    +

    baz

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- a + - b + - c + +- d + - e + - f +. +
    +
  • +

    a

    +
      +
    • b
    • +
    • c
    • +
    +
  • +
  • +

    d

    +
      +
    • e
    • +
    • f
    • +
    +
  • +
+```````````````````````````````` + + +# Inlines + +Inlines are parsed sequentially from the beginning of the character +stream to the end (left to right, in left-to-right languages). +Thus, for example, in + +```````````````````````````````` example +`hi`lo` +. +

hilo`

+```````````````````````````````` + +`hi` is parsed as code, leaving the backtick at the end as a literal +backtick. + + +## Backslash escapes + +Any ASCII punctuation character may be backslash-escaped: + +```````````````````````````````` example +\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~ +. +

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

+```````````````````````````````` + + +Backslashes before other characters are treated as literal +backslashes: + +```````````````````````````````` example +\→\A\a\ \3\φ\« +. +

\→\A\a\ \3\φ\«

+```````````````````````````````` + + +Escaped characters are treated as regular characters and do +not have their usual Markdown meanings: + +```````````````````````````````` example +\*not emphasized* +\
not a tag +\[not a link](/foo) +\`not code` +1\. not a list +\* not a list +\# not a heading +\[foo]: /url "not a reference" +\ö not a character entity +. +

*not emphasized* +<br/> not a tag +[not a link](/foo) +`not code` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +&ouml; not a character entity

+```````````````````````````````` + + +If a backslash is itself escaped, the following character is not: + +```````````````````````````````` example +\\*emphasis* +. +

\emphasis

+```````````````````````````````` + + +A backslash at the end of the line is a [hard line break]: + +```````````````````````````````` example +foo\ +bar +. +

foo
+bar

+```````````````````````````````` + + +Backslash escapes do not work in code blocks, code spans, autolinks, or +raw HTML: + +```````````````````````````````` example +`` \[\` `` +. +

\[\`

+```````````````````````````````` + + +```````````````````````````````` example + \[\] +. +
\[\]
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~ +\[\] +~~~ +. +
\[\]
+
+```````````````````````````````` + + +```````````````````````````````` example + +. +

http://example.com?find=\*

+```````````````````````````````` + + +```````````````````````````````` example + +. + +```````````````````````````````` + + +But they work in all other contexts, including URLs and link titles, +link references, and [info strings] in [fenced code blocks]: + +```````````````````````````````` example +[foo](/bar\* "ti\*tle") +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo] + +[foo]: /bar\* "ti\*tle" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +``` foo\+bar +foo +``` +. +
foo
+
+```````````````````````````````` + + + +## Entity and numeric character references + +Valid HTML entity references and numeric character references +can be used in place of the corresponding Unicode character, +with the following exceptions: + +- Entity and character references are not recognized in code + blocks and code spans. + +- Entity and character references cannot stand in place of + special characters that define structural elements in + CommonMark. For example, although `*` can be used + in place of a literal `*` character, `*` cannot replace + `*` in emphasis delimiters, bullet list markers, or thematic + breaks. + +Conforming CommonMark parsers need not store information about +whether a particular character was represented in the source +using a Unicode character or an entity reference. + +[Entity references](@) consist of `&` + any of the valid +HTML5 entity names + `;`. The +document +is used as an authoritative source for the valid entity +references and their corresponding code points. + +```````````````````````````````` example +  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸ +. +

  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸

+```````````````````````````````` + + +[Decimal numeric character +references](@) +consist of `&#` + a string of 1--7 arabic digits + `;`. A +numeric character reference is parsed as the corresponding +Unicode character. Invalid Unicode code points will be replaced by +the REPLACEMENT CHARACTER (`U+FFFD`). For security reasons, +the code point `U+0000` will also be replaced by `U+FFFD`. + +```````````````````````````````` example +# Ӓ Ϡ � +. +

# Ӓ Ϡ �

+```````````````````````````````` + + +[Hexadecimal numeric character +references](@) consist of `&#` + +either `X` or `x` + a string of 1-6 hexadecimal digits + `;`. +They too are parsed as the corresponding Unicode character (this +time specified with a hexadecimal numeral instead of decimal). + +```````````````````````````````` example +" ആ ಫ +. +

" ആ ಫ

+```````````````````````````````` + + +Here are some nonentities: + +```````````````````````````````` example +  &x; &#; &#x; +� +&#abcdef0; +&ThisIsNotDefined; &hi?; +. +

&nbsp &x; &#; &#x; +&#87654321; +&#abcdef0; +&ThisIsNotDefined; &hi?;

+```````````````````````````````` + + +Although HTML5 does accept some entity references +without a trailing semicolon (such as `©`), these are not +recognized here, because it makes the grammar too ambiguous: + +```````````````````````````````` example +© +. +

&copy

+```````````````````````````````` + + +Strings that are not on the list of HTML5 named entities are not +recognized as entity references either: + +```````````````````````````````` example +&MadeUpEntity; +. +

&MadeUpEntity;

+```````````````````````````````` + + +Entity and numeric character references are recognized in any +context besides code spans or code blocks, including +URLs, [link titles], and [fenced code block][] [info strings]: + +```````````````````````````````` example + +. + +```````````````````````````````` + + +```````````````````````````````` example +[foo](/föö "föö") +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo] + +[foo]: /föö "föö" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +``` föö +foo +``` +. +
foo
+
+```````````````````````````````` + + +Entity and numeric character references are treated as literal +text in code spans and code blocks: + +```````````````````````````````` example +`föö` +. +

f&ouml;&ouml;

+```````````````````````````````` + + +```````````````````````````````` example + föfö +. +
f&ouml;f&ouml;
+
+```````````````````````````````` + + +Entity and numeric character references cannot be used +in place of symbols indicating structure in CommonMark +documents. + +```````````````````````````````` example +*foo* +*foo* +. +

*foo* +foo

+```````````````````````````````` + +```````````````````````````````` example +* foo + +* foo +. +

* foo

+
    +
  • foo
  • +
+```````````````````````````````` + +```````````````````````````````` example +foo bar +. +

foo + +bar

+```````````````````````````````` + +```````````````````````````````` example + foo +. +

→foo

+```````````````````````````````` + + +```````````````````````````````` example +[a](url "tit") +. +

[a](url "tit")

+```````````````````````````````` + + +## Code spans + +A [backtick string](@) +is a string of one or more backtick characters (`` ` ``) that is neither +preceded nor followed by a backtick. + +A [code span](@) begins with a backtick string and ends with +a backtick string of equal length. The contents of the code span are +the characters between the two backtick strings, normalized in the +following ways: + +- First, [line endings] are converted to [spaces]. +- If the resulting string both begins *and* ends with a [space] + character, but does not consist entirely of [space] + characters, a single [space] character is removed from the + front and back. This allows you to include code that begins + or ends with backtick characters, which must be separated by + whitespace from the opening or closing backtick strings. + +This is a simple code span: + +```````````````````````````````` example +`foo` +. +

foo

+```````````````````````````````` + + +Here two backticks are used, because the code contains a backtick. +This example also illustrates stripping of a single leading and +trailing space: + +```````````````````````````````` example +`` foo ` bar `` +. +

foo ` bar

+```````````````````````````````` + + +This example shows the motivation for stripping leading and trailing +spaces: + +```````````````````````````````` example +` `` ` +. +

``

+```````````````````````````````` + +Note that only *one* space is stripped: + +```````````````````````````````` example +` `` ` +. +

``

+```````````````````````````````` + +The stripping only happens if the space is on both +sides of the string: + +```````````````````````````````` example +` a` +. +

a

+```````````````````````````````` + +Only [spaces], and not [unicode whitespace] in general, are +stripped in this way: + +```````````````````````````````` example +` b ` +. +

 b 

+```````````````````````````````` + +No stripping occurs if the code span contains only spaces: + +```````````````````````````````` example +` ` +` ` +. +

  +

+```````````````````````````````` + + +[Line endings] are treated like spaces: + +```````````````````````````````` example +`` +foo +bar +baz +`` +. +

foo bar baz

+```````````````````````````````` + +```````````````````````````````` example +`` +foo +`` +. +

foo

+```````````````````````````````` + + +Interior spaces are not collapsed: + +```````````````````````````````` example +`foo bar +baz` +. +

foo bar baz

+```````````````````````````````` + +Note that browsers will typically collapse consecutive spaces +when rendering `` elements, so it is recommended that +the following CSS be used: + + code{white-space: pre-wrap;} + + +Note that backslash escapes do not work in code spans. All backslashes +are treated literally: + +```````````````````````````````` example +`foo\`bar` +. +

foo\bar`

+```````````````````````````````` + + +Backslash escapes are never needed, because one can always choose a +string of *n* backtick characters as delimiters, where the code does +not contain any strings of exactly *n* backtick characters. + +```````````````````````````````` example +``foo`bar`` +. +

foo`bar

+```````````````````````````````` + +```````````````````````````````` example +` foo `` bar ` +. +

foo `` bar

+```````````````````````````````` + + +Code span backticks have higher precedence than any other inline +constructs except HTML tags and autolinks. Thus, for example, this is +not parsed as emphasized text, since the second `*` is part of a code +span: + +```````````````````````````````` example +*foo`*` +. +

*foo*

+```````````````````````````````` + + +And this is not parsed as a link: + +```````````````````````````````` example +[not a `link](/foo`) +. +

[not a link](/foo)

+```````````````````````````````` + + +Code spans, HTML tags, and autolinks have the same precedence. +Thus, this is code: + +```````````````````````````````` example +`` +. +

<a href="">`

+```````````````````````````````` + + +But this is an HTML tag: + +```````````````````````````````` example +
` +. +

`

+```````````````````````````````` + + +And this is code: + +```````````````````````````````` example +`` +. +

<http://foo.bar.baz>`

+```````````````````````````````` + + +But this is an autolink: + +```````````````````````````````` example +` +. +

http://foo.bar.`baz`

+```````````````````````````````` + + +When a backtick string is not closed by a matching backtick string, +we just have literal backticks: + +```````````````````````````````` example +```foo`` +. +

```foo``

+```````````````````````````````` + + +```````````````````````````````` example +`foo +. +

`foo

+```````````````````````````````` + +The following case also illustrates the need for opening and +closing backtick strings to be equal in length: + +```````````````````````````````` example +`foo``bar`` +. +

`foobar

+```````````````````````````````` + + +## Emphasis and strong emphasis + +John Gruber's original [Markdown syntax +description](http://daringfireball.net/projects/markdown/syntax#em) says: + +> Markdown treats asterisks (`*`) and underscores (`_`) as indicators of +> emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML +> `` tag; double `*`'s or `_`'s will be wrapped with an HTML `` +> tag. + +This is enough for most users, but these rules leave much undecided, +especially when it comes to nested emphasis. The original +`Markdown.pl` test suite makes it clear that triple `***` and +`___` delimiters can be used for strong emphasis, and most +implementations have also allowed the following patterns: + +``` markdown +***strong emph*** +***strong** in emph* +***emph* in strong** +**in strong *emph*** +*in emph **strong*** +``` + +The following patterns are less widely supported, but the intent +is clear and they are useful (especially in contexts like bibliography +entries): + +``` markdown +*emph *with emph* in it* +**strong **with strong** in it** +``` + +Many implementations have also restricted intraword emphasis to +the `*` forms, to avoid unwanted emphasis in words containing +internal underscores. (It is best practice to put these in code +spans, but users often do not.) + +``` markdown +internal emphasis: foo*bar*baz +no emphasis: foo_bar_baz +``` + +The rules given below capture all of these patterns, while allowing +for efficient parsing strategies that do not backtrack. + +First, some definitions. A [delimiter run](@) is either +a sequence of one or more `*` characters that is not preceded or +followed by a non-backslash-escaped `*` character, or a sequence +of one or more `_` characters that is not preceded or followed by +a non-backslash-escaped `_` character. + +A [left-flanking delimiter run](@) is +a [delimiter run] that is (1) not followed by [Unicode whitespace], +and either (2a) not followed by a [punctuation character], or +(2b) followed by a [punctuation character] and +preceded by [Unicode whitespace] or a [punctuation character]. +For purposes of this definition, the beginning and the end of +the line count as Unicode whitespace. + +A [right-flanking delimiter run](@) is +a [delimiter run] that is (1) not preceded by [Unicode whitespace], +and either (2a) not preceded by a [punctuation character], or +(2b) preceded by a [punctuation character] and +followed by [Unicode whitespace] or a [punctuation character]. +For purposes of this definition, the beginning and the end of +the line count as Unicode whitespace. + +Here are some examples of delimiter runs. + + - left-flanking but not right-flanking: + + ``` + ***abc + _abc + **"abc" + _"abc" + ``` + + - right-flanking but not left-flanking: + + ``` + abc*** + abc_ + "abc"** + "abc"_ + ``` + + - Both left and right-flanking: + + ``` + abc***def + "abc"_"def" + ``` + + - Neither left nor right-flanking: + + ``` + abc *** def + a _ b + ``` + +(The idea of distinguishing left-flanking and right-flanking +delimiter runs based on the character before and the character +after comes from Roopesh Chander's +[vfmd](http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags). +vfmd uses the terminology "emphasis indicator string" instead of "delimiter +run," and its rules for distinguishing left- and right-flanking runs +are a bit more complex than the ones given here.) + +The following rules define emphasis and strong emphasis: + +1. A single `*` character [can open emphasis](@) + iff (if and only if) it is part of a [left-flanking delimiter run]. + +2. A single `_` character [can open emphasis] iff + it is part of a [left-flanking delimiter run] + and either (a) not part of a [right-flanking delimiter run] + or (b) part of a [right-flanking delimiter run] + preceded by punctuation. + +3. A single `*` character [can close emphasis](@) + iff it is part of a [right-flanking delimiter run]. + +4. A single `_` character [can close emphasis] iff + it is part of a [right-flanking delimiter run] + and either (a) not part of a [left-flanking delimiter run] + or (b) part of a [left-flanking delimiter run] + followed by punctuation. + +5. A double `**` [can open strong emphasis](@) + iff it is part of a [left-flanking delimiter run]. + +6. A double `__` [can open strong emphasis] iff + it is part of a [left-flanking delimiter run] + and either (a) not part of a [right-flanking delimiter run] + or (b) part of a [right-flanking delimiter run] + preceded by punctuation. + +7. A double `**` [can close strong emphasis](@) + iff it is part of a [right-flanking delimiter run]. + +8. A double `__` [can close strong emphasis] iff + it is part of a [right-flanking delimiter run] + and either (a) not part of a [left-flanking delimiter run] + or (b) part of a [left-flanking delimiter run] + followed by punctuation. + +9. Emphasis begins with a delimiter that [can open emphasis] and ends + with a delimiter that [can close emphasis], and that uses the same + character (`_` or `*`) as the opening delimiter. The + opening and closing delimiters must belong to separate + [delimiter runs]. If one of the delimiters can both + open and close emphasis, then the sum of the lengths of the + delimiter runs containing the opening and closing delimiters + must not be a multiple of 3 unless both lengths are + multiples of 3. + +10. Strong emphasis begins with a delimiter that + [can open strong emphasis] and ends with a delimiter that + [can close strong emphasis], and that uses the same character + (`_` or `*`) as the opening delimiter. The + opening and closing delimiters must belong to separate + [delimiter runs]. If one of the delimiters can both open + and close strong emphasis, then the sum of the lengths of + the delimiter runs containing the opening and closing + delimiters must not be a multiple of 3 unless both lengths + are multiples of 3. + +11. A literal `*` character cannot occur at the beginning or end of + `*`-delimited emphasis or `**`-delimited strong emphasis, unless it + is backslash-escaped. + +12. A literal `_` character cannot occur at the beginning or end of + `_`-delimited emphasis or `__`-delimited strong emphasis, unless it + is backslash-escaped. + +Where rules 1--12 above are compatible with multiple parsings, +the following principles resolve ambiguity: + +13. The number of nestings should be minimized. Thus, for example, + an interpretation `...` is always preferred to + `...`. + +14. An interpretation `...` is always + preferred to `...`. + +15. When two potential emphasis or strong emphasis spans overlap, + so that the second begins before the first ends and ends after + the first ends, the first takes precedence. Thus, for example, + `*foo _bar* baz_` is parsed as `foo _bar baz_` rather + than `*foo bar* baz`. + +16. When there are two potential emphasis or strong emphasis spans + with the same closing delimiter, the shorter one (the one that + opens later) takes precedence. Thus, for example, + `**foo **bar baz**` is parsed as `**foo bar baz` + rather than `foo **bar baz`. + +17. Inline code spans, links, images, and HTML tags group more tightly + than emphasis. So, when there is a choice between an interpretation + that contains one of these elements and one that does not, the + former always wins. Thus, for example, `*[foo*](bar)` is + parsed as `*foo*` rather than as + `[foo](bar)`. + +These rules can be illustrated through a series of examples. + +Rule 1: + +```````````````````````````````` example +*foo bar* +. +

foo bar

+```````````````````````````````` + + +This is not emphasis, because the opening `*` is followed by +whitespace, and hence not part of a [left-flanking delimiter run]: + +```````````````````````````````` example +a * foo bar* +. +

a * foo bar*

+```````````````````````````````` + + +This is not emphasis, because the opening `*` is preceded +by an alphanumeric and followed by punctuation, and hence +not part of a [left-flanking delimiter run]: + +```````````````````````````````` example +a*"foo"* +. +

a*"foo"*

+```````````````````````````````` + + +Unicode nonbreaking spaces count as whitespace, too: + +```````````````````````````````` example +* a * +. +

* a *

+```````````````````````````````` + + +Intraword emphasis with `*` is permitted: + +```````````````````````````````` example +foo*bar* +. +

foobar

+```````````````````````````````` + + +```````````````````````````````` example +5*6*78 +. +

5678

+```````````````````````````````` + + +Rule 2: + +```````````````````````````````` example +_foo bar_ +. +

foo bar

+```````````````````````````````` + + +This is not emphasis, because the opening `_` is followed by +whitespace: + +```````````````````````````````` example +_ foo bar_ +. +

_ foo bar_

+```````````````````````````````` + + +This is not emphasis, because the opening `_` is preceded +by an alphanumeric and followed by punctuation: + +```````````````````````````````` example +a_"foo"_ +. +

a_"foo"_

+```````````````````````````````` + + +Emphasis with `_` is not allowed inside words: + +```````````````````````````````` example +foo_bar_ +. +

foo_bar_

+```````````````````````````````` + + +```````````````````````````````` example +5_6_78 +. +

5_6_78

+```````````````````````````````` + + +```````````````````````````````` example +пристаням_стремятся_ +. +

пристаням_стремятся_

+```````````````````````````````` + + +Here `_` does not generate emphasis, because the first delimiter run +is right-flanking and the second left-flanking: + +```````````````````````````````` example +aa_"bb"_cc +. +

aa_"bb"_cc

+```````````````````````````````` + + +This is emphasis, even though the opening delimiter is +both left- and right-flanking, because it is preceded by +punctuation: + +```````````````````````````````` example +foo-_(bar)_ +. +

foo-(bar)

+```````````````````````````````` + + +Rule 3: + +This is not emphasis, because the closing delimiter does +not match the opening delimiter: + +```````````````````````````````` example +_foo* +. +

_foo*

+```````````````````````````````` + + +This is not emphasis, because the closing `*` is preceded by +whitespace: + +```````````````````````````````` example +*foo bar * +. +

*foo bar *

+```````````````````````````````` + + +A newline also counts as whitespace: + +```````````````````````````````` example +*foo bar +* +. +

*foo bar +*

+```````````````````````````````` + + +This is not emphasis, because the second `*` is +preceded by punctuation and followed by an alphanumeric +(hence it is not part of a [right-flanking delimiter run]: + +```````````````````````````````` example +*(*foo) +. +

*(*foo)

+```````````````````````````````` + + +The point of this restriction is more easily appreciated +with this example: + +```````````````````````````````` example +*(*foo*)* +. +

(foo)

+```````````````````````````````` + + +Intraword emphasis with `*` is allowed: + +```````````````````````````````` example +*foo*bar +. +

foobar

+```````````````````````````````` + + + +Rule 4: + +This is not emphasis, because the closing `_` is preceded by +whitespace: + +```````````````````````````````` example +_foo bar _ +. +

_foo bar _

+```````````````````````````````` + + +This is not emphasis, because the second `_` is +preceded by punctuation and followed by an alphanumeric: + +```````````````````````````````` example +_(_foo) +. +

_(_foo)

+```````````````````````````````` + + +This is emphasis within emphasis: + +```````````````````````````````` example +_(_foo_)_ +. +

(foo)

+```````````````````````````````` + + +Intraword emphasis is disallowed for `_`: + +```````````````````````````````` example +_foo_bar +. +

_foo_bar

+```````````````````````````````` + + +```````````````````````````````` example +_пристаням_стремятся +. +

_пристаням_стремятся

+```````````````````````````````` + + +```````````````````````````````` example +_foo_bar_baz_ +. +

foo_bar_baz

+```````````````````````````````` + + +This is emphasis, even though the closing delimiter is +both left- and right-flanking, because it is followed by +punctuation: + +```````````````````````````````` example +_(bar)_. +. +

(bar).

+```````````````````````````````` + + +Rule 5: + +```````````````````````````````` example +**foo bar** +. +

foo bar

+```````````````````````````````` + + +This is not strong emphasis, because the opening delimiter is +followed by whitespace: + +```````````````````````````````` example +** foo bar** +. +

** foo bar**

+```````````````````````````````` + + +This is not strong emphasis, because the opening `**` is preceded +by an alphanumeric and followed by punctuation, and hence +not part of a [left-flanking delimiter run]: + +```````````````````````````````` example +a**"foo"** +. +

a**"foo"**

+```````````````````````````````` + + +Intraword strong emphasis with `**` is permitted: + +```````````````````````````````` example +foo**bar** +. +

foobar

+```````````````````````````````` + + +Rule 6: + +```````````````````````````````` example +__foo bar__ +. +

foo bar

+```````````````````````````````` + + +This is not strong emphasis, because the opening delimiter is +followed by whitespace: + +```````````````````````````````` example +__ foo bar__ +. +

__ foo bar__

+```````````````````````````````` + + +A newline counts as whitespace: +```````````````````````````````` example +__ +foo bar__ +. +

__ +foo bar__

+```````````````````````````````` + + +This is not strong emphasis, because the opening `__` is preceded +by an alphanumeric and followed by punctuation: + +```````````````````````````````` example +a__"foo"__ +. +

a__"foo"__

+```````````````````````````````` + + +Intraword strong emphasis is forbidden with `__`: + +```````````````````````````````` example +foo__bar__ +. +

foo__bar__

+```````````````````````````````` + + +```````````````````````````````` example +5__6__78 +. +

5__6__78

+```````````````````````````````` + + +```````````````````````````````` example +пристаням__стремятся__ +. +

пристаням__стремятся__

+```````````````````````````````` + + +```````````````````````````````` example +__foo, __bar__, baz__ +. +

foo, bar, baz

+```````````````````````````````` + + +This is strong emphasis, even though the opening delimiter is +both left- and right-flanking, because it is preceded by +punctuation: + +```````````````````````````````` example +foo-__(bar)__ +. +

foo-(bar)

+```````````````````````````````` + + + +Rule 7: + +This is not strong emphasis, because the closing delimiter is preceded +by whitespace: + +```````````````````````````````` example +**foo bar ** +. +

**foo bar **

+```````````````````````````````` + + +(Nor can it be interpreted as an emphasized `*foo bar *`, because of +Rule 11.) + +This is not strong emphasis, because the second `**` is +preceded by punctuation and followed by an alphanumeric: + +```````````````````````````````` example +**(**foo) +. +

**(**foo)

+```````````````````````````````` + + +The point of this restriction is more easily appreciated +with these examples: + +```````````````````````````````` example +*(**foo**)* +. +

(foo)

+```````````````````````````````` + + +```````````````````````````````` example +**Gomphocarpus (*Gomphocarpus physocarpus*, syn. +*Asclepias physocarpa*)** +. +

Gomphocarpus (Gomphocarpus physocarpus, syn. +Asclepias physocarpa)

+```````````````````````````````` + + +```````````````````````````````` example +**foo "*bar*" foo** +. +

foo "bar" foo

+```````````````````````````````` + + +Intraword emphasis: + +```````````````````````````````` example +**foo**bar +. +

foobar

+```````````````````````````````` + + +Rule 8: + +This is not strong emphasis, because the closing delimiter is +preceded by whitespace: + +```````````````````````````````` example +__foo bar __ +. +

__foo bar __

+```````````````````````````````` + + +This is not strong emphasis, because the second `__` is +preceded by punctuation and followed by an alphanumeric: + +```````````````````````````````` example +__(__foo) +. +

__(__foo)

+```````````````````````````````` + + +The point of this restriction is more easily appreciated +with this example: + +```````````````````````````````` example +_(__foo__)_ +. +

(foo)

+```````````````````````````````` + + +Intraword strong emphasis is forbidden with `__`: + +```````````````````````````````` example +__foo__bar +. +

__foo__bar

+```````````````````````````````` + + +```````````````````````````````` example +__пристаням__стремятся +. +

__пристаням__стремятся

+```````````````````````````````` + + +```````````````````````````````` example +__foo__bar__baz__ +. +

foo__bar__baz

+```````````````````````````````` + + +This is strong emphasis, even though the closing delimiter is +both left- and right-flanking, because it is followed by +punctuation: + +```````````````````````````````` example +__(bar)__. +. +

(bar).

+```````````````````````````````` + + +Rule 9: + +Any nonempty sequence of inline elements can be the contents of an +emphasized span. + +```````````````````````````````` example +*foo [bar](/url)* +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo +bar* +. +

foo +bar

+```````````````````````````````` + + +In particular, emphasis and strong emphasis can be nested +inside emphasis: + +```````````````````````````````` example +_foo __bar__ baz_ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +_foo _bar_ baz_ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +__foo_ bar_ +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo *bar** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo **bar** baz* +. +

foo bar baz

+```````````````````````````````` + +```````````````````````````````` example +*foo**bar**baz* +. +

foobarbaz

+```````````````````````````````` + +Note that in the preceding case, the interpretation + +``` markdown +

foobarbaz

+``` + + +is precluded by the condition that a delimiter that +can both open and close (like the `*` after `foo`) +cannot form emphasis if the sum of the lengths of +the delimiter runs containing the opening and +closing delimiters is a multiple of 3 unless +both lengths are multiples of 3. + + +For the same reason, we don't get two consecutive +emphasis sections in this example: + +```````````````````````````````` example +*foo**bar* +. +

foo**bar

+```````````````````````````````` + + +The same condition ensures that the following +cases are all strong emphasis nested inside +emphasis, even when the interior spaces are +omitted: + + +```````````````````````````````` example +***foo** bar* +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo **bar*** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo**bar*** +. +

foobar

+```````````````````````````````` + + +When the lengths of the interior closing and opening +delimiter runs are *both* multiples of 3, though, +they can match to create emphasis: + +```````````````````````````````` example +foo***bar***baz +. +

foobarbaz

+```````````````````````````````` + +```````````````````````````````` example +foo******bar*********baz +. +

foobar***baz

+```````````````````````````````` + + +Indefinite levels of nesting are possible: + +```````````````````````````````` example +*foo **bar *baz* bim** bop* +. +

foo bar baz bim bop

+```````````````````````````````` + + +```````````````````````````````` example +*foo [*bar*](/url)* +. +

foo bar

+```````````````````````````````` + + +There can be no empty emphasis or strong emphasis: + +```````````````````````````````` example +** is not an empty emphasis +. +

** is not an empty emphasis

+```````````````````````````````` + + +```````````````````````````````` example +**** is not an empty strong emphasis +. +

**** is not an empty strong emphasis

+```````````````````````````````` + + + +Rule 10: + +Any nonempty sequence of inline elements can be the contents of an +strongly emphasized span. + +```````````````````````````````` example +**foo [bar](/url)** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo +bar** +. +

foo +bar

+```````````````````````````````` + + +In particular, emphasis and strong emphasis can be nested +inside strong emphasis: + +```````````````````````````````` example +__foo _bar_ baz__ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +__foo __bar__ baz__ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +____foo__ bar__ +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo **bar**** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo *bar* baz** +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +**foo*bar*baz** +. +

foobarbaz

+```````````````````````````````` + + +```````````````````````````````` example +***foo* bar** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo *bar*** +. +

foo bar

+```````````````````````````````` + + +Indefinite levels of nesting are possible: + +```````````````````````````````` example +**foo *bar **baz** +bim* bop** +. +

foo bar baz +bim bop

+```````````````````````````````` + + +```````````````````````````````` example +**foo [*bar*](/url)** +. +

foo bar

+```````````````````````````````` + + +There can be no empty emphasis or strong emphasis: + +```````````````````````````````` example +__ is not an empty emphasis +. +

__ is not an empty emphasis

+```````````````````````````````` + + +```````````````````````````````` example +____ is not an empty strong emphasis +. +

____ is not an empty strong emphasis

+```````````````````````````````` + + + +Rule 11: + +```````````````````````````````` example +foo *** +. +

foo ***

+```````````````````````````````` + + +```````````````````````````````` example +foo *\** +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +foo *_* +. +

foo _

+```````````````````````````````` + + +```````````````````````````````` example +foo ***** +. +

foo *****

+```````````````````````````````` + + +```````````````````````````````` example +foo **\*** +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +foo **_** +. +

foo _

+```````````````````````````````` + + +Note that when delimiters do not match evenly, Rule 11 determines +that the excess literal `*` characters will appear outside of the +emphasis, rather than inside it: + +```````````````````````````````` example +**foo* +. +

*foo

+```````````````````````````````` + + +```````````````````````````````` example +*foo** +. +

foo*

+```````````````````````````````` + + +```````````````````````````````` example +***foo** +. +

*foo

+```````````````````````````````` + + +```````````````````````````````` example +****foo* +. +

***foo

+```````````````````````````````` + + +```````````````````````````````` example +**foo*** +. +

foo*

+```````````````````````````````` + + +```````````````````````````````` example +*foo**** +. +

foo***

+```````````````````````````````` + + + +Rule 12: + +```````````````````````````````` example +foo ___ +. +

foo ___

+```````````````````````````````` + + +```````````````````````````````` example +foo _\__ +. +

foo _

+```````````````````````````````` + + +```````````````````````````````` example +foo _*_ +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +foo _____ +. +

foo _____

+```````````````````````````````` + + +```````````````````````````````` example +foo __\___ +. +

foo _

+```````````````````````````````` + + +```````````````````````````````` example +foo __*__ +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +__foo_ +. +

_foo

+```````````````````````````````` + + +Note that when delimiters do not match evenly, Rule 12 determines +that the excess literal `_` characters will appear outside of the +emphasis, rather than inside it: + +```````````````````````````````` example +_foo__ +. +

foo_

+```````````````````````````````` + + +```````````````````````````````` example +___foo__ +. +

_foo

+```````````````````````````````` + + +```````````````````````````````` example +____foo_ +. +

___foo

+```````````````````````````````` + + +```````````````````````````````` example +__foo___ +. +

foo_

+```````````````````````````````` + + +```````````````````````````````` example +_foo____ +. +

foo___

+```````````````````````````````` + + +Rule 13 implies that if you want emphasis nested directly inside +emphasis, you must use different delimiters: + +```````````````````````````````` example +**foo** +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +*_foo_* +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +__foo__ +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +_*foo*_ +. +

foo

+```````````````````````````````` + + +However, strong emphasis within strong emphasis is possible without +switching delimiters: + +```````````````````````````````` example +****foo**** +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +____foo____ +. +

foo

+```````````````````````````````` + + + +Rule 13 can be applied to arbitrarily long sequences of +delimiters: + +```````````````````````````````` example +******foo****** +. +

foo

+```````````````````````````````` + + +Rule 14: + +```````````````````````````````` example +***foo*** +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +_____foo_____ +. +

foo

+```````````````````````````````` + + +Rule 15: + +```````````````````````````````` example +*foo _bar* baz_ +. +

foo _bar baz_

+```````````````````````````````` + + +```````````````````````````````` example +*foo __bar *baz bim__ bam* +. +

foo bar *baz bim bam

+```````````````````````````````` + + +Rule 16: + +```````````````````````````````` example +**foo **bar baz** +. +

**foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +*foo *bar baz* +. +

*foo bar baz

+```````````````````````````````` + + +Rule 17: + +```````````````````````````````` example +*[bar*](/url) +. +

*bar*

+```````````````````````````````` + + +```````````````````````````````` example +_foo [bar_](/url) +. +

_foo bar_

+```````````````````````````````` + + +```````````````````````````````` example +* +. +

*

+```````````````````````````````` + + +```````````````````````````````` example +** +. +

**

+```````````````````````````````` + + +```````````````````````````````` example +__ +. +

__

+```````````````````````````````` + + +```````````````````````````````` example +*a `*`* +. +

a *

+```````````````````````````````` + + +```````````````````````````````` example +_a `_`_ +. +

a _

+```````````````````````````````` + + +```````````````````````````````` example +**a +. +

**ahttp://foo.bar/?q=**

+```````````````````````````````` + + +```````````````````````````````` example +__a +. +

__ahttp://foo.bar/?q=__

+```````````````````````````````` + + + +## Links + +A link contains [link text] (the visible text), a [link destination] +(the URI that is the link destination), and optionally a [link title]. +There are two basic kinds of links in Markdown. In [inline links] the +destination and title are given immediately after the link text. In +[reference links] the destination and title are defined elsewhere in +the document. + +A [link text](@) consists of a sequence of zero or more +inline elements enclosed by square brackets (`[` and `]`). The +following rules apply: + +- Links may not contain other links, at any level of nesting. If + multiple otherwise valid link definitions appear nested inside each + other, the inner-most definition is used. + +- Brackets are allowed in the [link text] only if (a) they + are backslash-escaped or (b) they appear as a matched pair of brackets, + with an open bracket `[`, a sequence of zero or more inlines, and + a close bracket `]`. + +- Backtick [code spans], [autolinks], and raw [HTML tags] bind more tightly + than the brackets in link text. Thus, for example, + `` [foo`]` `` could not be a link text, since the second `]` + is part of a code span. + +- The brackets in link text bind more tightly than markers for + [emphasis and strong emphasis]. Thus, for example, `*[foo*](url)` is a link. + +A [link destination](@) consists of either + +- a sequence of zero or more characters between an opening `<` and a + closing `>` that contains no line breaks or unescaped + `<` or `>` characters, or + +- a nonempty sequence of characters that does not start with + `<`, does not include ASCII space or control characters, and + includes parentheses only if (a) they are backslash-escaped or + (b) they are part of a balanced pair of unescaped parentheses. + (Implementations may impose limits on parentheses nesting to + avoid performance issues, but at least three levels of nesting + should be supported.) + +A [link title](@) consists of either + +- a sequence of zero or more characters between straight double-quote + characters (`"`), including a `"` character only if it is + backslash-escaped, or + +- a sequence of zero or more characters between straight single-quote + characters (`'`), including a `'` character only if it is + backslash-escaped, or + +- a sequence of zero or more characters between matching parentheses + (`(...)`), including a `(` or `)` character only if it is + backslash-escaped. + +Although [link titles] may span multiple lines, they may not contain +a [blank line]. + +An [inline link](@) consists of a [link text] followed immediately +by a left parenthesis `(`, optional [whitespace], an optional +[link destination], an optional [link title] separated from the link +destination by [whitespace], optional [whitespace], and a right +parenthesis `)`. The link's text consists of the inlines contained +in the [link text] (excluding the enclosing square brackets). +The link's URI consists of the link destination, excluding enclosing +`<...>` if present, with backslash-escapes in effect as described +above. The link's title consists of the link title, excluding its +enclosing delimiters, with backslash-escapes in effect as described +above. + +Here is a simple inline link: + +```````````````````````````````` example +[link](/uri "title") +. +

link

+```````````````````````````````` + + +The title may be omitted: + +```````````````````````````````` example +[link](/uri) +. +

link

+```````````````````````````````` + + +Both the title and the destination may be omitted: + +```````````````````````````````` example +[link]() +. +

link

+```````````````````````````````` + + +```````````````````````````````` example +[link](<>) +. +

link

+```````````````````````````````` + +The destination can only contain spaces if it is +enclosed in pointy brackets: + +```````````````````````````````` example +[link](/my uri) +. +

[link](/my uri)

+```````````````````````````````` + +```````````````````````````````` example +[link](
) +. +

link

+```````````````````````````````` + +The destination cannot contain line breaks, +even if enclosed in pointy brackets: + +```````````````````````````````` example +[link](foo +bar) +. +

[link](foo +bar)

+```````````````````````````````` + +```````````````````````````````` example +[link]() +. +

[link]()

+```````````````````````````````` + +The destination can contain `)` if it is enclosed +in pointy brackets: + +```````````````````````````````` example +[a]() +. +

a

+```````````````````````````````` + +Pointy brackets that enclose links must be unescaped: + +```````````````````````````````` example +[link]() +. +

[link](<foo>)

+```````````````````````````````` + +These are not links, because the opening pointy bracket +is not matched properly: + +```````````````````````````````` example +[a]( +[a](c) +. +

[a](<b)c +[a](<b)c> +[a](c)

+```````````````````````````````` + +Parentheses inside the link destination may be escaped: + +```````````````````````````````` example +[link](\(foo\)) +. +

link

+```````````````````````````````` + +Any number of parentheses are allowed without escaping, as long as they are +balanced: + +```````````````````````````````` example +[link](foo(and(bar))) +. +

link

+```````````````````````````````` + +However, if you have unbalanced parentheses, you need to escape or use the +`<...>` form: + +```````````````````````````````` example +[link](foo\(and\(bar\)) +. +

link

+```````````````````````````````` + + +```````````````````````````````` example +[link]() +. +

link

+```````````````````````````````` + + +Parentheses and other symbols can also be escaped, as usual +in Markdown: + +```````````````````````````````` example +[link](foo\)\:) +. +

link

+```````````````````````````````` + + +A link can contain fragment identifiers and queries: + +```````````````````````````````` example +[link](#fragment) + +[link](http://example.com#fragment) + +[link](http://example.com?foo=3#frag) +. +

link

+

link

+

link

+```````````````````````````````` + + +Note that a backslash before a non-escapable character is +just a backslash: + +```````````````````````````````` example +[link](foo\bar) +. +

link

+```````````````````````````````` + + +URL-escaping should be left alone inside the destination, as all +URL-escaped characters are also valid URL characters. Entity and +numerical character references in the destination will be parsed +into the corresponding Unicode code points, as usual. These may +be optionally URL-escaped when written as HTML, but this spec +does not enforce any particular policy for rendering URLs in +HTML or other formats. Renderers may make different decisions +about how to escape or normalize URLs in the output. + +```````````````````````````````` example +[link](foo%20bä) +. +

link

+```````````````````````````````` + + +Note that, because titles can often be parsed as destinations, +if you try to omit the destination and keep the title, you'll +get unexpected results: + +```````````````````````````````` example +[link]("title") +. +

link

+```````````````````````````````` + + +Titles may be in single quotes, double quotes, or parentheses: + +```````````````````````````````` example +[link](/url "title") +[link](/url 'title') +[link](/url (title)) +. +

link +link +link

+```````````````````````````````` + + +Backslash escapes and entity and numeric character references +may be used in titles: + +```````````````````````````````` example +[link](/url "title \""") +. +

link

+```````````````````````````````` + + +Titles must be separated from the link using a [whitespace]. +Other [Unicode whitespace] like non-breaking space doesn't work. + +```````````````````````````````` example +[link](/url "title") +. +

link

+```````````````````````````````` + + +Nested balanced quotes are not allowed without escaping: + +```````````````````````````````` example +[link](/url "title "and" title") +. +

[link](/url "title "and" title")

+```````````````````````````````` + + +But it is easy to work around this by using a different quote type: + +```````````````````````````````` example +[link](/url 'title "and" title') +. +

link

+```````````````````````````````` + + +(Note: `Markdown.pl` did allow double quotes inside a double-quoted +title, and its test suite included a test demonstrating this. +But it is hard to see a good rationale for the extra complexity this +brings, since there are already many ways---backslash escaping, +entity and numeric character references, or using a different +quote type for the enclosing title---to write titles containing +double quotes. `Markdown.pl`'s handling of titles has a number +of other strange features. For example, it allows single-quoted +titles in inline links, but not reference links. And, in +reference links but not inline links, it allows a title to begin +with `"` and end with `)`. `Markdown.pl` 1.0.1 even allows +titles with no closing quotation mark, though 1.0.2b8 does not. +It seems preferable to adopt a simple, rational rule that works +the same way in inline links and link reference definitions.) + +[Whitespace] is allowed around the destination and title: + +```````````````````````````````` example +[link]( /uri + "title" ) +. +

link

+```````````````````````````````` + + +But it is not allowed between the link text and the +following parenthesis: + +```````````````````````````````` example +[link] (/uri) +. +

[link] (/uri)

+```````````````````````````````` + + +The link text may contain balanced brackets, but not unbalanced ones, +unless they are escaped: + +```````````````````````````````` example +[link [foo [bar]]](/uri) +. +

link [foo [bar]]

+```````````````````````````````` + + +```````````````````````````````` example +[link] bar](/uri) +. +

[link] bar](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +[link [bar](/uri) +. +

[link bar

+```````````````````````````````` + + +```````````````````````````````` example +[link \[bar](/uri) +. +

link [bar

+```````````````````````````````` + + +The link text may contain inline content: + +```````````````````````````````` example +[link *foo **bar** `#`*](/uri) +. +

link foo bar #

+```````````````````````````````` + + +```````````````````````````````` example +[![moon](moon.jpg)](/uri) +. +

moon

+```````````````````````````````` + + +However, links may not contain other links, at any level of nesting. + +```````````````````````````````` example +[foo [bar](/uri)](/uri) +. +

[foo bar](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +[foo *[bar [baz](/uri)](/uri)*](/uri) +. +

[foo [bar baz](/uri)](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +![[[foo](uri1)](uri2)](uri3) +. +

[foo](uri2)

+```````````````````````````````` + + +These cases illustrate the precedence of link text grouping over +emphasis grouping: + +```````````````````````````````` example +*[foo*](/uri) +. +

*foo*

+```````````````````````````````` + + +```````````````````````````````` example +[foo *bar](baz*) +. +

foo *bar

+```````````````````````````````` + + +Note that brackets that *aren't* part of links do not take +precedence: + +```````````````````````````````` example +*foo [bar* baz] +. +

foo [bar baz]

+```````````````````````````````` + + +These cases illustrate the precedence of HTML tags, code spans, +and autolinks over link grouping: + +```````````````````````````````` example +[foo +. +

[foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo`](/uri)` +. +

[foo](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +[foo +. +

[foohttp://example.com/?search=](uri)

+```````````````````````````````` + + +There are three kinds of [reference link](@)s: +[full](#full-reference-link), [collapsed](#collapsed-reference-link), +and [shortcut](#shortcut-reference-link). + +A [full reference link](@) +consists of a [link text] immediately followed by a [link label] +that [matches] a [link reference definition] elsewhere in the document. + +A [link label](@) begins with a left bracket (`[`) and ends +with the first right bracket (`]`) that is not backslash-escaped. +Between these brackets there must be at least one [non-whitespace character]. +Unescaped square bracket characters are not allowed inside the +opening and closing square brackets of [link labels]. A link +label can have at most 999 characters inside the square +brackets. + +One label [matches](@) +another just in case their normalized forms are equal. To normalize a +label, strip off the opening and closing brackets, +perform the *Unicode case fold*, strip leading and trailing +[whitespace] and collapse consecutive internal +[whitespace] to a single space. If there are multiple +matching reference link definitions, the one that comes first in the +document is used. (It is desirable in such cases to emit a warning.) + +The contents of the first link label are parsed as inlines, which are +used as the link's text. The link's URI and title are provided by the +matching [link reference definition]. + +Here is a simple example: + +```````````````````````````````` example +[foo][bar] + +[bar]: /url "title" +. +

foo

+```````````````````````````````` + + +The rules for the [link text] are the same as with +[inline links]. Thus: + +The link text may contain balanced brackets, but not unbalanced ones, +unless they are escaped: + +```````````````````````````````` example +[link [foo [bar]]][ref] + +[ref]: /uri +. +

link [foo [bar]]

+```````````````````````````````` + + +```````````````````````````````` example +[link \[bar][ref] + +[ref]: /uri +. +

link [bar

+```````````````````````````````` + + +The link text may contain inline content: + +```````````````````````````````` example +[link *foo **bar** `#`*][ref] + +[ref]: /uri +. +

link foo bar #

+```````````````````````````````` + + +```````````````````````````````` example +[![moon](moon.jpg)][ref] + +[ref]: /uri +. +

moon

+```````````````````````````````` + + +However, links may not contain other links, at any level of nesting. + +```````````````````````````````` example +[foo [bar](/uri)][ref] + +[ref]: /uri +. +

[foo bar]ref

+```````````````````````````````` + + +```````````````````````````````` example +[foo *bar [baz][ref]*][ref] + +[ref]: /uri +. +

[foo bar baz]ref

+```````````````````````````````` + + +(In the examples above, we have two [shortcut reference links] +instead of one [full reference link].) + +The following cases illustrate the precedence of link text grouping over +emphasis grouping: + +```````````````````````````````` example +*[foo*][ref] + +[ref]: /uri +. +

*foo*

+```````````````````````````````` + + +```````````````````````````````` example +[foo *bar][ref] + +[ref]: /uri +. +

foo *bar

+```````````````````````````````` + + +These cases illustrate the precedence of HTML tags, code spans, +and autolinks over link grouping: + +```````````````````````````````` example +[foo + +[ref]: /uri +. +

[foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo`][ref]` + +[ref]: /uri +. +

[foo][ref]

+```````````````````````````````` + + +```````````````````````````````` example +[foo + +[ref]: /uri +. +

[foohttp://example.com/?search=][ref]

+```````````````````````````````` + + +Matching is case-insensitive: + +```````````````````````````````` example +[foo][BaR] + +[bar]: /url "title" +. +

foo

+```````````````````````````````` + + +Unicode case fold is used: + +```````````````````````````````` example +[Толпой][Толпой] is a Russian word. + +[ТОЛПОЙ]: /url +. +

Толпой is a Russian word.

+```````````````````````````````` + + +Consecutive internal [whitespace] is treated as one space for +purposes of determining matching: + +```````````````````````````````` example +[Foo + bar]: /url + +[Baz][Foo bar] +. +

Baz

+```````````````````````````````` + + +No [whitespace] is allowed between the [link text] and the +[link label]: + +```````````````````````````````` example +[foo] [bar] + +[bar]: /url "title" +. +

[foo] bar

+```````````````````````````````` + + +```````````````````````````````` example +[foo] +[bar] + +[bar]: /url "title" +. +

[foo] +bar

+```````````````````````````````` + + +This is a departure from John Gruber's original Markdown syntax +description, which explicitly allows whitespace between the link +text and the link label. It brings reference links in line with +[inline links], which (according to both original Markdown and +this spec) cannot have whitespace after the link text. More +importantly, it prevents inadvertent capture of consecutive +[shortcut reference links]. If whitespace is allowed between the +link text and the link label, then in the following we will have +a single reference link, not two shortcut reference links, as +intended: + +``` markdown +[foo] +[bar] + +[foo]: /url1 +[bar]: /url2 +``` + +(Note that [shortcut reference links] were introduced by Gruber +himself in a beta version of `Markdown.pl`, but never included +in the official syntax description. Without shortcut reference +links, it is harmless to allow space between the link text and +link label; but once shortcut references are introduced, it is +too dangerous to allow this, as it frequently leads to +unintended results.) + +When there are multiple matching [link reference definitions], +the first is used: + +```````````````````````````````` example +[foo]: /url1 + +[foo]: /url2 + +[bar][foo] +. +

bar

+```````````````````````````````` + + +Note that matching is performed on normalized strings, not parsed +inline content. So the following does not match, even though the +labels define equivalent inline content: + +```````````````````````````````` example +[bar][foo\!] + +[foo!]: /url +. +

[bar][foo!]

+```````````````````````````````` + + +[Link labels] cannot contain brackets, unless they are +backslash-escaped: + +```````````````````````````````` example +[foo][ref[] + +[ref[]: /uri +. +

[foo][ref[]

+

[ref[]: /uri

+```````````````````````````````` + + +```````````````````````````````` example +[foo][ref[bar]] + +[ref[bar]]: /uri +. +

[foo][ref[bar]]

+

[ref[bar]]: /uri

+```````````````````````````````` + + +```````````````````````````````` example +[[[foo]]] + +[[[foo]]]: /url +. +

[[[foo]]]

+

[[[foo]]]: /url

+```````````````````````````````` + + +```````````````````````````````` example +[foo][ref\[] + +[ref\[]: /uri +. +

foo

+```````````````````````````````` + + +Note that in this example `]` is not backslash-escaped: + +```````````````````````````````` example +[bar\\]: /uri + +[bar\\] +. +

bar\

+```````````````````````````````` + + +A [link label] must contain at least one [non-whitespace character]: + +```````````````````````````````` example +[] + +[]: /uri +. +

[]

+

[]: /uri

+```````````````````````````````` + + +```````````````````````````````` example +[ + ] + +[ + ]: /uri +. +

[ +]

+

[ +]: /uri

+```````````````````````````````` + + +A [collapsed reference link](@) +consists of a [link label] that [matches] a +[link reference definition] elsewhere in the +document, followed by the string `[]`. +The contents of the first link label are parsed as inlines, +which are used as the link's text. The link's URI and title are +provided by the matching reference link definition. Thus, +`[foo][]` is equivalent to `[foo][foo]`. + +```````````````````````````````` example +[foo][] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[*foo* bar][] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +The link labels are case-insensitive: + +```````````````````````````````` example +[Foo][] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + + +As with full reference links, [whitespace] is not +allowed between the two sets of brackets: + +```````````````````````````````` example +[foo] +[] + +[foo]: /url "title" +. +

foo +[]

+```````````````````````````````` + + +A [shortcut reference link](@) +consists of a [link label] that [matches] a +[link reference definition] elsewhere in the +document and is not followed by `[]` or a link label. +The contents of the first link label are parsed as inlines, +which are used as the link's text. The link's URI and title +are provided by the matching link reference definition. +Thus, `[foo]` is equivalent to `[foo][]`. + +```````````````````````````````` example +[foo] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[*foo* bar] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +[[*foo* bar]] + +[*foo* bar]: /url "title" +. +

[foo bar]

+```````````````````````````````` + + +```````````````````````````````` example +[[bar [foo] + +[foo]: /url +. +

[[bar foo

+```````````````````````````````` + + +The link labels are case-insensitive: + +```````````````````````````````` example +[Foo] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + +A space after the link text should be preserved: + +```````````````````````````````` example +[foo] bar + +[foo]: /url +. +

foo bar

+```````````````````````````````` + + +If you just want bracketed text, you can backslash-escape the +opening bracket to avoid links: + +```````````````````````````````` example +\[foo] + +[foo]: /url "title" +. +

[foo]

+```````````````````````````````` + + +Note that this is a link, because a link label ends with the first +following closing bracket: + +```````````````````````````````` example +[foo*]: /url + +*[foo*] +. +

*foo*

+```````````````````````````````` + + +Full and compact references take precedence over shortcut +references: + +```````````````````````````````` example +[foo][bar] + +[foo]: /url1 +[bar]: /url2 +. +

foo

+```````````````````````````````` + +```````````````````````````````` example +[foo][] + +[foo]: /url1 +. +

foo

+```````````````````````````````` + +Inline links also take precedence: + +```````````````````````````````` example +[foo]() + +[foo]: /url1 +. +

foo

+```````````````````````````````` + +```````````````````````````````` example +[foo](not a link) + +[foo]: /url1 +. +

foo(not a link)

+```````````````````````````````` + +In the following case `[bar][baz]` is parsed as a reference, +`[foo]` as normal text: + +```````````````````````````````` example +[foo][bar][baz] + +[baz]: /url +. +

[foo]bar

+```````````````````````````````` + + +Here, though, `[foo][bar]` is parsed as a reference, since +`[bar]` is defined: + +```````````````````````````````` example +[foo][bar][baz] + +[baz]: /url1 +[bar]: /url2 +. +

foobaz

+```````````````````````````````` + + +Here `[foo]` is not parsed as a shortcut reference, because it +is followed by a link label (even though `[bar]` is not defined): + +```````````````````````````````` example +[foo][bar][baz] + +[baz]: /url1 +[foo]: /url2 +. +

[foo]bar

+```````````````````````````````` + + + +## Images + +Syntax for images is like the syntax for links, with one +difference. Instead of [link text], we have an +[image description](@). The rules for this are the +same as for [link text], except that (a) an +image description starts with `![` rather than `[`, and +(b) an image description may contain links. +An image description has inline elements +as its contents. When an image is rendered to HTML, +this is standardly used as the image's `alt` attribute. + +```````````````````````````````` example +![foo](/url "title") +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![foo *bar*] + +[foo *bar*]: train.jpg "train & tracks" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo ![bar](/url)](/url2) +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo [bar](/url)](/url2) +. +

foo bar

+```````````````````````````````` + + +Though this spec is concerned with parsing, not rendering, it is +recommended that in rendering to HTML, only the plain string content +of the [image description] be used. Note that in +the above example, the alt attribute's value is `foo bar`, not `foo +[bar](/url)` or `foo bar`. Only the plain string +content is rendered, without formatting. + +```````````````````````````````` example +![foo *bar*][] + +[foo *bar*]: train.jpg "train & tracks" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo *bar*][foobar] + +[FOOBAR]: train.jpg "train & tracks" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo](train.jpg) +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +My ![foo bar](/path/to/train.jpg "title" ) +. +

My foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo]() +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![](/url) +. +

+```````````````````````````````` + + +Reference-style: + +```````````````````````````````` example +![foo][bar] + +[bar]: /url +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![foo][bar] + +[BAR]: /url +. +

foo

+```````````````````````````````` + + +Collapsed: + +```````````````````````````````` example +![foo][] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![*foo* bar][] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +The labels are case-insensitive: + +```````````````````````````````` example +![Foo][] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + +As with reference links, [whitespace] is not allowed +between the two sets of brackets: + +```````````````````````````````` example +![foo] +[] + +[foo]: /url "title" +. +

foo +[]

+```````````````````````````````` + + +Shortcut: + +```````````````````````````````` example +![foo] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![*foo* bar] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +Note that link labels cannot contain unescaped brackets: + +```````````````````````````````` example +![[foo]] + +[[foo]]: /url "title" +. +

![[foo]]

+

[[foo]]: /url "title"

+```````````````````````````````` + + +The link labels are case-insensitive: + +```````````````````````````````` example +![Foo] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + +If you just want a literal `!` followed by bracketed text, you can +backslash-escape the opening `[`: + +```````````````````````````````` example +!\[foo] + +[foo]: /url "title" +. +

![foo]

+```````````````````````````````` + + +If you want a link after a literal `!`, backslash-escape the +`!`: + +```````````````````````````````` example +\![foo] + +[foo]: /url "title" +. +

!foo

+```````````````````````````````` + + +## Autolinks + +[Autolink](@)s are absolute URIs and email addresses inside +`<` and `>`. They are parsed as links, with the URL or email address +as the link label. + +A [URI autolink](@) consists of `<`, followed by an +[absolute URI] followed by `>`. It is parsed as +a link to the URI, with the URI as the link's label. + +An [absolute URI](@), +for these purposes, consists of a [scheme] followed by a colon (`:`) +followed by zero or more characters other than ASCII +[whitespace] and control characters, `<`, and `>`. If +the URI includes these characters, they must be percent-encoded +(e.g. `%20` for a space). + +For purposes of this spec, a [scheme](@) is any sequence +of 2--32 characters beginning with an ASCII letter and followed +by any combination of ASCII letters, digits, or the symbols plus +("+"), period ("."), or hyphen ("-"). + +Here are some valid autolinks: + +```````````````````````````````` example + +. +

http://foo.bar.baz

+```````````````````````````````` + + +```````````````````````````````` example + +. +

http://foo.bar.baz/test?q=hello&id=22&boolean

+```````````````````````````````` + + +```````````````````````````````` example + +. +

irc://foo.bar:2233/baz

+```````````````````````````````` + + +Uppercase is also fine: + +```````````````````````````````` example + +. +

MAILTO:FOO@BAR.BAZ

+```````````````````````````````` + + +Note that many strings that count as [absolute URIs] for +purposes of this spec are not valid URIs, because their +schemes are not registered or because of other problems +with their syntax: + +```````````````````````````````` example + +. +

a+b+c:d

+```````````````````````````````` + + +```````````````````````````````` example + +. +

made-up-scheme://foo,bar

+```````````````````````````````` + + +```````````````````````````````` example + +. +

http://../

+```````````````````````````````` + + +```````````````````````````````` example + +. +

localhost:5001/foo

+```````````````````````````````` + + +Spaces are not allowed in autolinks: + +```````````````````````````````` example + +. +

<http://foo.bar/baz bim>

+```````````````````````````````` + + +Backslash-escapes do not work inside autolinks: + +```````````````````````````````` example + +. +

http://example.com/\[\

+```````````````````````````````` + + +An [email autolink](@) +consists of `<`, followed by an [email address], +followed by `>`. The link's label is the email address, +and the URL is `mailto:` followed by the email address. + +An [email address](@), +for these purposes, is anything that matches +the [non-normative regex from the HTML5 +spec](https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email)): + + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? + (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + +Examples of email autolinks: + +```````````````````````````````` example + +. +

foo@bar.example.com

+```````````````````````````````` + + +```````````````````````````````` example + +. +

foo+special@Bar.baz-bar0.com

+```````````````````````````````` + + +Backslash-escapes do not work inside email autolinks: + +```````````````````````````````` example + +. +

<foo+@bar.example.com>

+```````````````````````````````` + + +These are not autolinks: + +```````````````````````````````` example +<> +. +

<>

+```````````````````````````````` + + +```````````````````````````````` example +< http://foo.bar > +. +

< http://foo.bar >

+```````````````````````````````` + + +```````````````````````````````` example + +. +

<m:abc>

+```````````````````````````````` + + +```````````````````````````````` example + +. +

<foo.bar.baz>

+```````````````````````````````` + + +```````````````````````````````` example +http://example.com +. +

http://example.com

+```````````````````````````````` + + +```````````````````````````````` example +foo@bar.example.com +. +

foo@bar.example.com

+```````````````````````````````` + + +## Raw HTML + +Text between `<` and `>` that looks like an HTML tag is parsed as a +raw HTML tag and will be rendered in HTML without escaping. +Tag and attribute names are not limited to current HTML tags, +so custom tags (and even, say, DocBook tags) may be used. + +Here is the grammar for tags: + +A [tag name](@) consists of an ASCII letter +followed by zero or more ASCII letters, digits, or +hyphens (`-`). + +An [attribute](@) consists of [whitespace], +an [attribute name], and an optional +[attribute value specification]. + +An [attribute name](@) +consists of an ASCII letter, `_`, or `:`, followed by zero or more ASCII +letters, digits, `_`, `.`, `:`, or `-`. (Note: This is the XML +specification restricted to ASCII. HTML5 is laxer.) + +An [attribute value specification](@) +consists of optional [whitespace], +a `=` character, optional [whitespace], and an [attribute +value]. + +An [attribute value](@) +consists of an [unquoted attribute value], +a [single-quoted attribute value], or a [double-quoted attribute value]. + +An [unquoted attribute value](@) +is a nonempty string of characters not +including [whitespace], `"`, `'`, `=`, `<`, `>`, or `` ` ``. + +A [single-quoted attribute value](@) +consists of `'`, zero or more +characters not including `'`, and a final `'`. + +A [double-quoted attribute value](@) +consists of `"`, zero or more +characters not including `"`, and a final `"`. + +An [open tag](@) consists of a `<` character, a [tag name], +zero or more [attributes], optional [whitespace], an optional `/` +character, and a `>` character. + +A [closing tag](@) consists of the string ``. + +An [HTML comment](@) consists of ``, +where *text* does not start with `>` or `->`, does not end with `-`, +and does not contain `--`. (See the +[HTML5 spec](http://www.w3.org/TR/html5/syntax.html#comments).) + +A [processing instruction](@) +consists of the string ``, and the string +`?>`. + +A [declaration](@) consists of the +string ``, and the character `>`. + +A [CDATA section](@) consists of +the string ``, and the string `]]>`. + +An [HTML tag](@) consists of an [open tag], a [closing tag], +an [HTML comment], a [processing instruction], a [declaration], +or a [CDATA section]. + +Here are some simple open tags: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Empty elements: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +[Whitespace] is allowed: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +With attributes: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Custom tag names can be used: + +```````````````````````````````` example +Foo +. +

Foo

+```````````````````````````````` + + +Illegal tag names, not parsed as HTML: + +```````````````````````````````` example +<33> <__> +. +

<33> <__>

+```````````````````````````````` + + +Illegal attribute names: + +```````````````````````````````` example +
+. +

<a h*#ref="hi">

+```````````````````````````````` + + +Illegal attribute values: + +```````````````````````````````` example +
+. +

</a href="foo">

+```````````````````````````````` + + +Comments: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +foo +. +

foo <!-- not a comment -- two hyphens -->

+```````````````````````````````` + + +Not comments: + +```````````````````````````````` example +foo foo --> + +foo +. +

foo <!--> foo -->

+

foo <!-- foo--->

+```````````````````````````````` + + +Processing instructions: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +Declarations: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +CDATA sections: + +```````````````````````````````` example +foo &<]]> +. +

foo &<]]>

+```````````````````````````````` + + +Entity and numeric character references are preserved in HTML +attributes: + +```````````````````````````````` example +foo
+. +

foo

+```````````````````````````````` + + +Backslash escapes do not work in HTML attributes: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example + +. +

<a href=""">

+```````````````````````````````` + + +## Hard line breaks + +A line break (not in a code span or HTML tag) that is preceded +by two or more spaces and does not occur at the end of a block +is parsed as a [hard line break](@) (rendered +in HTML as a `
` tag): + +```````````````````````````````` example +foo +baz +. +

foo
+baz

+```````````````````````````````` + + +For a more visible alternative, a backslash before the +[line ending] may be used instead of two spaces: + +```````````````````````````````` example +foo\ +baz +. +

foo
+baz

+```````````````````````````````` + + +More than two spaces can be used: + +```````````````````````````````` example +foo +baz +. +

foo
+baz

+```````````````````````````````` + + +Leading spaces at the beginning of the next line are ignored: + +```````````````````````````````` example +foo + bar +. +

foo
+bar

+```````````````````````````````` + + +```````````````````````````````` example +foo\ + bar +. +

foo
+bar

+```````````````````````````````` + + +Line breaks can occur inside emphasis, links, and other constructs +that allow inline content: + +```````````````````````````````` example +*foo +bar* +. +

foo
+bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo\ +bar* +. +

foo
+bar

+```````````````````````````````` + + +Line breaks do not occur inside code spans + +```````````````````````````````` example +`code +span` +. +

code span

+```````````````````````````````` + + +```````````````````````````````` example +`code\ +span` +. +

code\ span

+```````````````````````````````` + + +or HTML tags: + +```````````````````````````````` example +
+. +

+```````````````````````````````` + + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Hard line breaks are for separating inline content within a block. +Neither syntax for hard line breaks works at the end of a paragraph or +other block element: + +```````````````````````````````` example +foo\ +. +

foo\

+```````````````````````````````` + + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +### foo\ +. +

foo\

+```````````````````````````````` + + +```````````````````````````````` example +### foo +. +

foo

+```````````````````````````````` + + +## Soft line breaks + +A regular line break (not in a code span or HTML tag) that is not +preceded by two or more spaces or a backslash is parsed as a +[softbreak](@). (A softbreak may be rendered in HTML either as a +[line ending] or as a space. The result will be the same in +browsers. In the examples here, a [line ending] will be used.) + +```````````````````````````````` example +foo +baz +. +

foo +baz

+```````````````````````````````` + + +Spaces at the end of the line and beginning of the next line are +removed: + +```````````````````````````````` example +foo + baz +. +

foo +baz

+```````````````````````````````` + + +A conforming parser may render a soft line break in HTML either as a +line break or as a space. + +A renderer may also provide an option to render soft line breaks +as hard line breaks. + +## Textual content + +Any characters not given an interpretation by the above rules will +be parsed as plain textual content. + +```````````````````````````````` example +hello $.;'there +. +

hello $.;'there

+```````````````````````````````` + + +```````````````````````````````` example +Foo χρῆν +. +

Foo χρῆν

+```````````````````````````````` + + +Internal spaces are preserved verbatim: + +```````````````````````````````` example +Multiple spaces +. +

Multiple spaces

+```````````````````````````````` + + + + +# Appendix: A parsing strategy + +In this appendix we describe some features of the parsing strategy +used in the CommonMark reference implementations. + +## Overview + +Parsing has two phases: + +1. In the first phase, lines of input are consumed and the block +structure of the document---its division into paragraphs, block quotes, +list items, and so on---is constructed. Text is assigned to these +blocks but not parsed. Link reference definitions are parsed and a +map of links is constructed. + +2. In the second phase, the raw text contents of paragraphs and headings +are parsed into sequences of Markdown inline elements (strings, +code spans, links, emphasis, and so on), using the map of link +references constructed in phase 1. + +At each point in processing, the document is represented as a tree of +**blocks**. The root of the tree is a `document` block. The `document` +may have any number of other blocks as **children**. These children +may, in turn, have other blocks as children. The last child of a block +is normally considered **open**, meaning that subsequent lines of input +can alter its contents. (Blocks that are not open are **closed**.) +Here, for example, is a possible document tree, with the open blocks +marked by arrows: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + list_item + paragraph + "Qui *quodsi iracundia*" + -> list_item + -> paragraph + "aliquando id" +``` + +## Phase 1: block structure + +Each line that is processed has an effect on this tree. The line is +analyzed and, depending on its contents, the document may be altered +in one or more of the following ways: + +1. One or more open blocks may be closed. +2. One or more new blocks may be created as children of the + last open block. +3. Text may be added to the last (deepest) open block remaining + on the tree. + +Once a line has been incorporated into the tree in this way, +it can be discarded, so input can be read in a stream. + +For each line, we follow this procedure: + +1. First we iterate through the open blocks, starting with the +root document, and descending through last children down to the last +open block. Each block imposes a condition that the line must satisfy +if the block is to remain open. For example, a block quote requires a +`>` character. A paragraph requires a non-blank line. +In this phase we may match all or just some of the open +blocks. But we cannot close unmatched blocks yet, because we may have a +[lazy continuation line]. + +2. Next, after consuming the continuation markers for existing +blocks, we look for new block starts (e.g. `>` for a block quote). +If we encounter a new block start, we close any blocks unmatched +in step 1 before creating the new block as a child of the last +matched block. + +3. Finally, we look at the remainder of the line (after block +markers like `>`, list markers, and indentation have been consumed). +This is text that can be incorporated into the last open +block (a paragraph, code block, heading, or raw HTML). + +Setext headings are formed when we see a line of a paragraph +that is a [setext heading underline]. + +Reference link definitions are detected when a paragraph is closed; +the accumulated text lines are parsed to see if they begin with +one or more reference link definitions. Any remainder becomes a +normal paragraph. + +We can see how this works by considering how the tree above is +generated by four lines of Markdown: + +``` markdown +> Lorem ipsum dolor +sit amet. +> - Qui *quodsi iracundia* +> - aliquando id +``` + +At the outset, our document model is just + +``` tree +-> document +``` + +The first line of our text, + +``` markdown +> Lorem ipsum dolor +``` + +causes a `block_quote` block to be created as a child of our +open `document` block, and a `paragraph` block as a child of +the `block_quote`. Then the text is added to the last open +block, the `paragraph`: + +``` tree +-> document + -> block_quote + -> paragraph + "Lorem ipsum dolor" +``` + +The next line, + +``` markdown +sit amet. +``` + +is a "lazy continuation" of the open `paragraph`, so it gets added +to the paragraph's text: + +``` tree +-> document + -> block_quote + -> paragraph + "Lorem ipsum dolor\nsit amet." +``` + +The third line, + +``` markdown +> - Qui *quodsi iracundia* +``` + +causes the `paragraph` block to be closed, and a new `list` block +opened as a child of the `block_quote`. A `list_item` is also +added as a child of the `list`, and a `paragraph` as a child of +the `list_item`. The text is then added to the new `paragraph`: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + -> list_item + -> paragraph + "Qui *quodsi iracundia*" +``` + +The fourth line, + +``` markdown +> - aliquando id +``` + +causes the `list_item` (and its child the `paragraph`) to be closed, +and a new `list_item` opened up as child of the `list`. A `paragraph` +is added as a child of the new `list_item`, to contain the text. +We thus obtain the final tree: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + list_item + paragraph + "Qui *quodsi iracundia*" + -> list_item + -> paragraph + "aliquando id" +``` + +## Phase 2: inline structure + +Once all of the input has been parsed, all open blocks are closed. + +We then "walk the tree," visiting every node, and parse raw +string contents of paragraphs and headings as inlines. At this +point we have seen all the link reference definitions, so we can +resolve reference links as we go. + +``` tree +document + block_quote + paragraph + str "Lorem ipsum dolor" + softbreak + str "sit amet." + list (type=bullet tight=true bullet_char=-) + list_item + paragraph + str "Qui " + emph + str "quodsi iracundia" + list_item + paragraph + str "aliquando id" +``` + +Notice how the [line ending] in the first paragraph has +been parsed as a `softbreak`, and the asterisks in the first list item +have become an `emph`. + +### An algorithm for parsing nested emphasis and links + +By far the trickiest part of inline parsing is handling emphasis, +strong emphasis, links, and images. This is done using the following +algorithm. + +When we're parsing inlines and we hit either + +- a run of `*` or `_` characters, or +- a `[` or `![` + +we insert a text node with these symbols as its literal content, and we +add a pointer to this text node to the [delimiter stack](@). + +The [delimiter stack] is a doubly linked list. Each +element contains a pointer to a text node, plus information about + +- the type of delimiter (`[`, `![`, `*`, `_`) +- the number of delimiters, +- whether the delimiter is "active" (all are active to start), and +- whether the delimiter is a potential opener, a potential closer, + or both (which depends on what sort of characters precede + and follow the delimiters). + +When we hit a `]` character, we call the *look for link or image* +procedure (see below). + +When we hit the end of the input, we call the *process emphasis* +procedure (see below), with `stack_bottom` = NULL. + +#### *look for link or image* + +Starting at the top of the delimiter stack, we look backwards +through the stack for an opening `[` or `![` delimiter. + +- If we don't find one, we return a literal text node `]`. + +- If we do find one, but it's not *active*, we remove the inactive + delimiter from the stack, and return a literal text node `]`. + +- If we find one and it's active, then we parse ahead to see if + we have an inline link/image, reference link/image, compact reference + link/image, or shortcut reference link/image. + + + If we don't, then we remove the opening delimiter from the + delimiter stack and return a literal text node `]`. + + + If we do, then + + * We return a link or image node whose children are the inlines + after the text node pointed to by the opening delimiter. + + * We run *process emphasis* on these inlines, with the `[` opener + as `stack_bottom`. + + * We remove the opening delimiter. + + * If we have a link (and not an image), we also set all + `[` delimiters before the opening delimiter to *inactive*. (This + will prevent us from getting links within links.) + +#### *process emphasis* + +Parameter `stack_bottom` sets a lower bound to how far we +descend in the [delimiter stack]. If it is NULL, we can +go all the way to the bottom. Otherwise, we stop before +visiting `stack_bottom`. + +Let `current_position` point to the element on the [delimiter stack] +just above `stack_bottom` (or the first element if `stack_bottom` +is NULL). + +We keep track of the `openers_bottom` for each delimiter +type (`*`, `_`) and each length of the closing delimiter run +(modulo 3). Initialize this to `stack_bottom`. + +Then we repeat the following until we run out of potential +closers: + +- Move `current_position` forward in the delimiter stack (if needed) + until we find the first potential closer with delimiter `*` or `_`. + (This will be the potential closer closest + to the beginning of the input -- the first one in parse order.) + +- Now, look back in the stack (staying above `stack_bottom` and + the `openers_bottom` for this delimiter type) for the + first matching potential opener ("matching" means same delimiter). + +- If one is found: + + + Figure out whether we have emphasis or strong emphasis: + if both closer and opener spans have length >= 2, we have + strong, otherwise regular. + + + Insert an emph or strong emph node accordingly, after + the text node corresponding to the opener. + + + Remove any delimiters between the opener and closer from + the delimiter stack. + + + Remove 1 (for regular emph) or 2 (for strong emph) delimiters + from the opening and closing text nodes. If they become empty + as a result, remove them and remove the corresponding element + of the delimiter stack. If the closing node is removed, reset + `current_position` to the next element in the stack. + +- If none is found: + + + Set `openers_bottom` to the element before `current_position`. + (We know that there are no openers for this kind of closer up to and + including this point, so this puts a lower bound on future searches.) + + + If the closer at `current_position` is not a potential opener, + remove it from the delimiter stack (since we know it can't + be a closer either). + + + Advance `current_position` to the next element in the stack. + +After we're done, we remove all delimiters above `stack_bottom` from the +delimiter stack. + diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs index 598cd6c8d..02d34926f 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs index bb0e5c5b1..0681a5398 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestBackslashEscapeInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs index 9ade5e03b..1f4a4f2ea 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs index 4e1357383..7710c6ef6 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestEmphasisInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs index 879a98f5f..13a1d3299 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlEntityInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs index 65d84ace3..15d51f18c 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestHtmlInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs index 71e4a1ddb..e93418419 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestImageInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs index 866f20e25..416fcb702 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLineBreakInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs index 8c3a29d29..a4f5a7b6b 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs index f78c66cb3..d87eb59b5 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs.Inlines { diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 1b9594430..1ee5bd3ef 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -19,9 +19,8 @@ In order: - ~~support SetextHeading~~ - ~~support LinkReferenceDefinition~~ - ~~support link parsing~~ -- support AutolinkInline -- `\0` -- generate spec examples as tests for roundtrip +- ~~support AutolinkInline~~ +- ~~generate spec examples as tests for roundtrip~~ - fix `TODO: RTP: ` - check char.IsWhitespace() calls - check char.IsNewline() calls @@ -37,6 +36,7 @@ In order: - run perf test - create todo list with perf optimization focus points - optimize perf +- `\0` - merge from main - document how trivia are handled generically and specifically - write tree comparison tests? diff --git a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs index 4ad665d08..5442d95d7 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index 4f4be5e6c..564d554bc 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs b/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs index 94aaf880b..a2cce15c8 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs @@ -7,17 +7,6 @@ namespace Markdig.Tests.RoundtripSpecs { internal static class TestHelper { - internal static void RoundTrip(string markdown) - { - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs index 8fbff4fa5..d40217cf7 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestHtmlBlock.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index 23c7c1bde..8912636a9 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index 76291100c..8133af06e 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index 247d08888..c6a0d6327 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index b46f19f08..eddadf8aa 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 9534cc018..ffa46c061 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs index 17ed72726..581bcc8a0 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs index 2e9794f23..e1eaf1576 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestThematicBreak.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index 1bb0ab000..4b209b3d8 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using static Markdig.Tests.RoundtripSpecs.TestHelper; +using static Markdig.Tests.TestRoundtrip; namespace Markdig.Tests.RoundtripSpecs { diff --git a/src/Markdig.Tests/TestRoundtrip.cs b/src/Markdig.Tests/TestRoundtrip.cs new file mode 100644 index 000000000..1e6a57f06 --- /dev/null +++ b/src/Markdig.Tests/TestRoundtrip.cs @@ -0,0 +1,28 @@ +using Markdig.Renderers.Normalize; +using Markdig.Syntax; +using NUnit.Framework; +using System.IO; + +namespace Markdig.Tests +{ + internal static class TestRoundtrip + { + internal static void TestSpec(string markdownText, string expected, string extensions) + { + RoundTrip(markdownText); + } + + internal static void RoundTrip(string markdown) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var sw = new StringWriter(); + var nr = new NormalizeRenderer(sw); + + nr.Write(markdownDocument); + + Assert.AreEqual(markdown, sw.ToString()); + } + } +} diff --git a/src/SpecFileGen/Program.cs b/src/SpecFileGen/Program.cs index 74cde2e24..46ac46971 100644 --- a/src/SpecFileGen/Program.cs +++ b/src/SpecFileGen/Program.cs @@ -17,7 +17,8 @@ enum RendererType { Html, Normalize, - PlainText + PlainText, + Roundtrip } class Spec @@ -36,6 +37,7 @@ public Spec(string name, string fileName, string extensions, RendererType render if (rendererType == RendererType.Html) Path += "Specs"; else if (rendererType == RendererType.Normalize) Path += "NormalizeSpecs"; else if (rendererType == RendererType.PlainText) Path += "PlainTextSpecs"; + else if (rendererType == RendererType.Roundtrip) Path += "RoundtripSpecs"; Path += "/" + fileName; OutputPath = System.IO.Path.ChangeExtension(Path, "generated.cs"); Extensions = extensions; @@ -52,6 +54,11 @@ class PlainTextSpec : Spec public PlainTextSpec(string name, string fileName, string extensions) : base(name, fileName, extensions, rendererType: RendererType.PlainText) { } } + class RoundtripSpec : Spec + { + public RoundtripSpec(string name, string fileName, string extensions) + : base(name, fileName, extensions, rendererType: RendererType.Roundtrip) { } + } // NOTE: Beware of Copy/Pasting spec files - some characters may change (non-breaking space into space)! static readonly Spec[] Specs = new[] @@ -85,6 +92,8 @@ public PlainTextSpec(string name, string fileName, string extensions) new NormalizeSpec("Headings", "Headings.md", ""), new PlainTextSpec("Sample", "SamplePlainText.md", ""), + + new RoundtripSpec("Roundtrip", "CommonMark.md", ""), }; static void Main() @@ -183,6 +192,7 @@ static string ParseSpecification(Spec spec, string specSource) Write("namespace Markdig.Tests.Specs."); if (spec.RendererType == RendererType.Normalize) Write("Normalize."); else if (spec.RendererType == RendererType.PlainText) Write("PlainText."); + else if (spec.RendererType == RendererType.Roundtrip) Write("Roundtrip."); Line(CompressedName(spec.Name).Replace('.', '_')); Line("{"); @@ -333,6 +343,7 @@ static void WriteTest(string name, string compressedName, int number, string ext if (rendererType == RendererType.Html) Write("TestParser"); else if (rendererType == RendererType.Normalize) Write("TestNormalize"); else if (rendererType == RendererType.PlainText) Write("TestPlainText"); + else if (rendererType == RendererType.Roundtrip) Write("TestRoundtrip"); Write(".TestSpec(\""); for (int i = markdownOffset; i < markdownEnd; i++) { From 05dd543d1638862100b3c6526958e6ba3e8e357e Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 12:17:33 +0200 Subject: [PATCH 043/120] fix HtmlBlock tests --- .../RoundtripSpecs/Inlines/TestCodeInline.cs | 14 +++++++ .../Renderers/Normalize/CodeBlockRenderer.cs | 2 +- .../Renderers/Normalize/HtmlBlockRenderer.cs | 3 +- .../Normalize/Inlines/CodeInlineRenderer.cs | 28 ------------- .../LinkReferenceDefinitionGroupRenderer.cs | 2 +- .../Renderers/Normalize/NormalizeRenderer.cs | 39 +++++++------------ 6 files changed, 30 insertions(+), 58 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs index 1f4a4f2ea..17609c618 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestCodeInline.cs @@ -59,5 +59,19 @@ public void TestParagraph(string value) { RoundTrip(value); } + + [TestCase("`\na\n`")] + [TestCase("`\na\r`")] + [TestCase("`\na\r\n`")] + [TestCase("`\ra\r`")] + [TestCase("`\ra\n`")] + [TestCase("`\ra\r\n`")] + [TestCase("`\r\na\n`")] + [TestCase("`\r\na\r`")] + [TestCase("`\r\na\r\n`")] + public void Test_Newlines(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 0ca0abbe9..3d0d05e9e 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -57,7 +57,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) */ renderer.WriteLine(fencedCodeBlock.InfoNewline); - renderer.WriteLeafRawLines(obj, true); + renderer.WriteLeafRawLines(obj); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); renderer.WriteLine(obj.Newline); diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index 9c27f0323..fd0773a5a 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -11,8 +11,7 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { renderer.RenderLinesBefore(obj); - renderer.WriteLeafRawLines(obj, false, false); - renderer.WriteLine(obj.Newline); + renderer.WriteLeafRawLines(obj); renderer.RenderLinesAfter(obj); } } diff --git a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs index b1fdce46b..c9ac4663b 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs @@ -14,22 +14,6 @@ public class CodeInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, CodeInline obj) { - var delimiterCount = 0; - //for (var i = 0; i < obj.Content.Length; i++) - //{ - // var index = obj.Content.IndexOf(obj.Delimiter, i); - // if (index == -1) break; - - // var count = 1; - // for (i = index + 1; i < obj.Content.Length; i++) - // { - // if (obj.Content[i] == obj.Delimiter) count++; - // else break; - // } - - // if (delimiterCount < count) - // delimiterCount = count; - //} var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); renderer.Write(delimiterRun); if (obj.FirstAndLastWasSpace) @@ -38,19 +22,7 @@ protected override void Write(NormalizeRenderer renderer, CodeInline obj) } if (obj.Content.Length != 0) { - if (obj.Content[0] == obj.Delimiter) - { - renderer.Write(' '); - } renderer.Write(obj.Content); - if (obj.Content[obj.Content.Length - 1] == obj.Delimiter) - { - renderer.Write(' '); - } - } - else - { - renderer.Write(' '); } if (obj.FirstAndLastWasSpace) { diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs index 8d1a7ca2d..381c338e7 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs @@ -12,7 +12,7 @@ protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitio { renderer.EnsureLine(); renderer.WriteChildren(obj); - renderer.FinishBlock(false); + //renderer.FinishBlock(false); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index d58acff1f..cb40b7e32 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -50,14 +50,6 @@ public NormalizeRenderer(TextWriter writer, NormalizeOptions options = null) : b public bool CompactParagraph { get; set; } - public void FinishBlock(bool derp = false) - { - if (!IsLastInContainer) - { - WriteLine(); // TODO: remove this method? - } - } - ///// ///// Writes the attached on the specified . ///// @@ -125,7 +117,7 @@ public void FinishBlock(bool derp = false) /// if set to true write end of lines. /// Whether to write indents. /// This instance - public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool indent = false) + public void WriteLeafRawLines(LeafBlock leafBlock) { if (leafBlock == null) ThrowHelper.ArgumentNullException_leafBlock(); if (leafBlock.Lines.Lines != null) @@ -134,39 +126,34 @@ public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfL var slices = lines.Lines; for (int i = 0; i < lines.Count; i++) { - - if (indent) - { - Write(" "); - } - var slice = slices[i].Slice; Write(ref slice); WriteLine(slice.Newline); } } - return this; } public void RenderLinesBefore(Block block) { - if (block.LinesBefore != null) + if (block.LinesBefore == null) { - foreach (var line in block.LinesBefore) - { - WriteLine(line.Newline); - } + return; + } + foreach (var line in block.LinesBefore) + { + WriteLine(line.Newline); } } public void RenderLinesAfter(Block block) { - if (block.LinesAfter != null) + if (block.LinesAfter == null) { - foreach (var line in block.LinesAfter) - { - WriteLine(line.Newline); - } + return; + } + foreach (var line in block.LinesAfter) + { + WriteLine(line.Newline); } } } From 225e30843824af09ccfa5e0201c6bf53a353b2c7 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 12:40:57 +0200 Subject: [PATCH 044/120] fix AtxHeading tests --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 2 ++ src/Markdig/Parsers/HeadingBlockParser.cs | 2 ++ src/Markdig/Parsers/InlineProcessor.cs | 2 +- src/Markdig/Renderers/Normalize/HeadingRenderer.cs | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 1ee5bd3ef..f3a752c02 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -5,6 +5,8 @@ Roundtrip support allows parsing of Markdown to subsequently render it back to M - newlines before blocks are assigned to that block - whitespace starting on a line is assigned to the block on that line - assigning whitespace *before* a node has precedence over asigning whitespace *after* a node +- whitespace vs trivia + - AtxHeading can have #s after the title, white are including as trivia ## Quoteblock Quoteblocks may have different syntactical characters applied per line. That is, some lines belonging to a Quoteblock may and others **may not** contain the quote marker character `>`. Each line of a Quoteblock therefore stores the quote marker character and its surrounding whitespace. diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 9143abe3a..fa545fb66 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -92,6 +92,7 @@ public override BlockState TryOpen(BlockProcessor processor) // The optional closing sequence of #s must be preceded by a space and may be followed by spaces only. int endState = 0; int countClosingTags = 0; + int sourceEnd = processor.Line.End; for (int i = processor.Line.End; i >= processor.Line.Start - 1; i--) // Go up to Start - 1 in order to match the space after the first ### { c = processor.Line.Text[i]; @@ -128,6 +129,7 @@ public override BlockState TryOpen(BlockProcessor processor) // Setup the source end position of this element headingBlock.Span.End = processor.Line.End; + headingBlock.AfterWhitespace = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); // We expect a single line, so don't continue return BlockState.Break; diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 0af82ca51..439c50af6 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -278,7 +278,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) //} } - if (leafBlock is HeadingBlock heading && heading.IsSetext) + if (leafBlock is HeadingBlock) { // TODO: RTP: delegate to block? } diff --git a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs index 70d8bee10..39c6c0240 100644 --- a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs @@ -50,6 +50,8 @@ protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) renderer.Write(obj.BeforeWhitespace); renderer.Write(headingText).Write(' '); renderer.WriteLeafInline(obj); + renderer.Write(obj.AfterWhitespace); + renderer.WriteLine(obj.Newline); renderer.RenderLinesAfter(obj); } From 810ae49cc730b729862fe9b03e9d254aa4709d1b Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 13:08:40 +0200 Subject: [PATCH 045/120] fix SetextHeading tests --- src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs | 4 ++++ src/Markdig/Parsers/BlockProcessor.cs | 9 +-------- src/Markdig/Parsers/ListBlockParser.cs | 3 +++ src/Markdig/Parsers/ParagraphBlockParser.cs | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs index 581bcc8a0..f367c368f 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestSetextHeading.cs @@ -41,6 +41,10 @@ public void Test(string value) [TestCase("h1\r===\r\n")] [TestCase("h1\n===\r\n")] [TestCase("h1\r\n===\r\n")] + + [TestCase("h1\n===\n\n\nh2---\n\n")] + [TestCase("h1\r===\r\r\rh2---\r\r")] + [TestCase("h1\r\n===\r\n\r\n\r\nh2---\r\n\r\n")] public void TestNewline(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index f7222efe9..8418f7577 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -180,21 +180,14 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo public int WhitespaceStart { get; set; } - // NOTE: Line.Text is full source, not the line (!) public StringSlice PopBeforeWhitespace(int end) { + // NOTE: Line.Text is full source, not the line (!) var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); WhitespaceStart = end + 1; return stringSlice; } - public StringSlice PopBeforeWhitespace(int start, int end) - { - var stringSlice = new StringSlice(Line.Text, start, end); - WhitespaceStart = end + 1; - return stringSlice; - } - public List BeforeLines { get; set; } public bool TrackTrivia { get => trackTrivia; set => trackTrivia = value; } diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index b7100479a..111c6cced 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -209,6 +209,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) return BlockState.None; } var bulletLength = 1; // TODO: RTP: fix for ordered + var savedWhitespaceStart = state.WhitespaceStart; var whitespaceBefore = state.PopBeforeWhitespace(state.Start - bulletLength - 1); state.WhitespaceStart = state.Start; @@ -231,6 +232,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) if (!c.IsSpaceOrTab()) { state.GoToColumn(initColumn); + state.WhitespaceStart = savedWhitespaceStart; // restore changed WhitespaceStart state return BlockState.None; } @@ -261,6 +263,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) state.IsOpen(previousParagraph) && listInfo.BulletType == '1' && listInfo.OrderedStart != "1") { state.GoToColumn(initColumn); + state.WhitespaceStart = savedWhitespaceStart; // restore changed WhitespaceStart state return BlockState.None; } } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index c042146e0..c08ee73db 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -115,15 +115,15 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Span = new SourceSpan(paragraph.Span.Start, line.Start), Level = level, Lines = paragraph.Lines, - BeforeWhitespace = state.PopBeforeWhitespace(sourcePosition - 1), + BeforeWhitespace = state.PopBeforeWhitespace(sourcePosition - 1), // remove dashes AfterWhitespace = new StringSlice(state.Line.Text, state.Start, line.End), - LinesBefore = state.UseLinesBefore(), + LinesBefore = paragraph.LinesBefore, Newline = state.Line.Newline, IsSetext = true, HeaderCharCount = count, SetextNewline = paragraph.Newline, }; - //heading.Lines.Trim(); + //heading.Lines.Trim(); // TODO: RTP: fix // Remove the paragraph as a pending block state.NewBlocks.Push(heading); From 777ac71bd1fd6fc8e9fe37dc6d94dae33a28efec Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 14:11:47 +0200 Subject: [PATCH 046/120] fix HardLineBreak, CodeInline tests --- src/Markdig/Parsers/Inlines/CodeInlineParser.cs | 11 +++++++---- src/Markdig/Parsers/Inlines/EscapeInlineParser.cs | 10 ++++++++-- .../Normalize/Inlines/CodeInlineRenderer.cs | 10 +--------- src/Markdig/Syntax/Inlines/CodeInline.cs | 2 ++ src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs | 12 ++++++++++++ 5 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs diff --git a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs index 03de579a9..4631c1e91 100644 --- a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs @@ -35,6 +35,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // Match the opened sticks int openSticks = slice.CountAndSkipChar(match); + int contentStart = slice.Start; int closeSticks = 0; char c = slice.CurrentChar; @@ -53,6 +54,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // whitespace from the opening or closing backtick strings. bool allSpace = true; + var contentEnd = -1; while (c != '\0') { @@ -64,6 +66,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (c == match) { + contentEnd = slice.Start; closeSticks = slice.CountAndSkipChar(match); if (openSticks == closeSticks) @@ -87,7 +90,6 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } bool isMatching = false; - bool firstAndLastWasSpace = false; if (closeSticks == openSticks) { string content; @@ -96,7 +98,6 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (!allSpace && builder.Length > 2 && builder[0] == ' ' && builder[builder.Length - 1] == ' ') { content = builder.ToString(1, builder.Length - 2); - firstAndLastWasSpace = true; } else { @@ -104,15 +105,17 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } int delimiterCount = Math.Min(openSticks, closeSticks); + var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column); + var spanEnd = processor.GetSourcePosition(slice.Start - 1); processor.Inline = new CodeInline() { Delimiter = match, Content = content, - Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), + ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1), + Span = new SourceSpan(spanStart, spanEnd), Line = line, Column = column, DelimiterCount = delimiterCount, - FirstAndLastWasSpace = firstAndLastWasSpace }; isMatching = true; } diff --git a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs index d59c06b69..320499f2d 100644 --- a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs @@ -41,15 +41,21 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } // A backslash at the end of the line is a [hard line break]: - if (c == '\n') + if (c == '\n' || c == '\r') { + var newline = c == '\n' ? Newline.LineFeed : Newline.CarriageReturn; + if (c == '\r' && slice.PeekChar() == '\n') + { + newline = Newline.CarriageReturnLineFeed; + } processor.Inline = new LineBreakInline() { IsHard = true, IsBackslash = true, Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, Line = line, - Column = column + Column = column, + Newline = newline }; processor.Inline.Span.End = processor.Inline.Span.Start + 1; slice.NextChar(); diff --git a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs index c9ac4663b..327c97a97 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs @@ -16,17 +16,9 @@ protected override void Write(NormalizeRenderer renderer, CodeInline obj) { var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); renderer.Write(delimiterRun); - if (obj.FirstAndLastWasSpace) - { - renderer.Write(' '); - } if (obj.Content.Length != 0) { - renderer.Write(obj.Content); - } - if (obj.FirstAndLastWasSpace) - { - renderer.Write(' '); + renderer.Write(obj.ContentWithTrivia); } renderer.Write(delimiterRun); } diff --git a/src/Markdig/Syntax/Inlines/CodeInline.cs b/src/Markdig/Syntax/Inlines/CodeInline.cs index 0d0bb63c5..7aa6bff84 100644 --- a/src/Markdig/Syntax/Inlines/CodeInline.cs +++ b/src/Markdig/Syntax/Inlines/CodeInline.cs @@ -29,6 +29,8 @@ public class CodeInline : LeafInline /// public string Content { get; set; } + public StringSlice ContentWithTrivia { get; set; } + public bool FirstAndLastWasSpace { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs b/src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs new file mode 100644 index 000000000..1a4d11dfb --- /dev/null +++ b/src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; + +namespace Markdig.Syntax.Inlines +{ + [DebuggerDisplay("\\{Character}")] + public class EscapedCharacterInline : LeafInline + { + public char Character { get; set; } + + public override string ToString() => $@"\\{Character}"; + } +} From 2583463ea2265e8af05473a3b173f9a2d17c4625 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 15:05:41 +0200 Subject: [PATCH 047/120] fix broken links --- src/Markdig/Helpers/CharHelper.cs | 2 +- src/Markdig/Helpers/StringLineGroup.cs | 2 +- src/Markdig/Parsers/ParagraphBlockParser.cs | 11 ++++++++--- .../Normalize/LinkReferenceDefinitionGroupRenderer.cs | 2 -- .../Normalize/LinkReferenceDefinitionRenderer.cs | 8 ++++++++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index 789c3a52e..edac1a31d 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -225,7 +225,7 @@ internal static bool IsSpaceOrPunctuation(this char c) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNewLine(this char c) { - return c == '\n'; + return c == '\n'; // TODO: RTP: check } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index 03011b7ee..e23caf0e8 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -323,7 +323,7 @@ public readonly char PeekChar(int offset = 1) if (offset == slice.Length) { - return '\n'; + return '\n'; // TODO: RTP: fix } Debug.Assert(offset < slice.Length); diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index c08ee73db..b27ea7270 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -58,7 +58,7 @@ public override bool Close(BlockProcessor processor, Block block) if (processor.TrackTrivia) { - TryMatchLinkReferenceDefinitionWhitespace(ref lines, processor); + TryMatchLinkReferenceDefinitionWhitespace(ref lines, processor, paragraph); } else { @@ -196,7 +196,7 @@ private static bool TryMatchLinkReferenceDefinition(ref StringLineGroup lines, B return atLeastOneFound; } - private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGroup lines, BlockProcessor state) + private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGroup lines, BlockProcessor state, ParagraphBlock paragraph) { bool atLeastOneFound = false; @@ -217,9 +217,13 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou // Correct the locations of each field lrd.Line = lines.Lines[0].Line; - var text = lines.ToString(); + var text = lines.Lines[0].Slice.Text; int startPosition = lines.Lines[0].Slice.Start; + whitespaceBeforeLabel = whitespaceBeforeLabel.MoveForward(startPosition); + whitespaceBeforeUrl = whitespaceBeforeUrl.MoveForward(startPosition); + whitespaceBeforeTitle = whitespaceBeforeTitle.MoveForward(startPosition); + whitespaceAfterTitle = whitespaceAfterTitle.MoveForward(startPosition); lrd.Span = lrd.Span.MoveForward(startPosition); lrd.BeforeWhitespace = new StringSlice(text, whitespaceBeforeLabel.Start, whitespaceBeforeLabel.End); lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); @@ -228,6 +232,7 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou lrd.WhitespaceBeforeTitle = new StringSlice(text, whitespaceBeforeTitle.Start, whitespaceBeforeTitle.End); lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); lrd.AfterWhitespace = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + lrd.LinesBefore = paragraph.LinesBefore; lines = iterator.Remaining(); } diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs index 381c338e7..a7c2dbf7b 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs @@ -10,9 +10,7 @@ public class LinkReferenceDefinitionGroupRenderer : NormalizeObjectRenderer'); + } renderer.Write(linkDef.WhitespaceBeforeTitle); if (linkDef.Title != null) From c5b260f7089379bf5f5bf427fc4085f988cfe152 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 16:09:24 +0200 Subject: [PATCH 048/120] fix rendering of empty lines with whitespace --- .../RoundtripSpecs/Inlines/TestLinkInline.cs | 8 ++++++++ src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs | 12 ++++++++++++ src/Markdig/Parsers/BlockProcessor.cs | 8 ++------ src/Markdig/Renderers/Normalize/NormalizeRenderer.cs | 2 ++ src/Markdig/Renderers/Normalize/ParagraphRenderer.cs | 1 + 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs index a4f5a7b6b..479d80768 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -201,6 +201,14 @@ public void Test_PointyBrackets(string value) RoundTrip(value); } + [TestCase("[*a*][a]")] + [TestCase("[a][b]")] + [TestCase("[a][]")] + public void Test_Inlines(string value) + { + RoundTrip(value); + } + // | [ a ]( b " t " ) | [TestCase(" [ a ]( b \" t \" ) ")] [TestCase("\v[\va\v](\vb\v\"\vt\v\"\v)\v")] diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index eddadf8aa..6e38b9ad7 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -202,5 +202,17 @@ public void TestNewline(string value) { RoundTrip(value); } + + [TestCase(" \n")] + [TestCase(" \r")] + [TestCase(" \r\n")] + + [TestCase(" \np")] + [TestCase(" \rp")] + [TestCase(" \r\np")] + public void Test_WhitespaceWithNewline(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 8418f7577..2c0f25167 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -743,11 +743,6 @@ private void TryOpenBlocks() } } - private void RestartBeforeLines() - { - BeforeLines = null; - } - /// /// Tries to open new blocks using the specified list of /// @@ -762,7 +757,8 @@ private bool TryOpenBlocks(BlockParser[] parsers) { BeforeLines ??= new List(); Line.Start = StartBeforeIndent; - BeforeLines.Add(Line); + var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); + BeforeLines.Add(line); ContinueProcessingLine = false; break; } diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index cb40b7e32..8e4e455c5 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -141,6 +141,7 @@ public void RenderLinesBefore(Block block) } foreach (var line in block.LinesBefore) { + Write(line); WriteLine(line.Newline); } } @@ -153,6 +154,7 @@ public void RenderLinesAfter(Block block) } foreach (var line in block.LinesAfter) { + Write(line); WriteLine(line.Newline); } } diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index d7f28778a..88e17866e 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -19,6 +19,7 @@ protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragra renderer.RenderLinesBefore(paragraph); renderer.Write(paragraph.BeforeWhitespace); renderer.WriteLeafInline(paragraph); + //renderer.Write(paragraph.Newline); renderer.RenderLinesAfter(paragraph); } } From cd798b8d9595b9d396f1a6898a67c0afe40510fc Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 18 Oct 2020 22:40:17 +0200 Subject: [PATCH 049/120] fix newlines after links --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 10 ++++++++++ src/Markdig/Parsers/InlineProcessor.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index f3a752c02..dad7b4891 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -14,6 +14,16 @@ Quoteblocks may have different syntactical characters applied per line. That is, ## Lists - beforewhitespace on list item +## Trivia +- whitespace + - ` ` (space) + - `\t` + - `\f` + - `\v` +- trailing `#` +- TODO: ThematicBreak +- TODO: link url `>`, link title `(`, `'`, `"` + # TODO In order: - ~~`p\n p`: affects many tests~~ diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 439c50af6..d55886a6d 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -285,7 +285,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) else { var newline = leafBlock.Newline; - container.AppendChild(new LineBreakInline { Newline = newline }); + leafBlock.Inline.AppendChild(new LineBreakInline { Newline = newline }); } Inline = null; From 352443d9cd25b2c9362e508681cb7466a4c46abd Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 11:32:15 +0200 Subject: [PATCH 050/120] fix unclosed FencedCodeBlock in QuoteBlock --- src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 3d0d05e9e..754cd41b2 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -60,8 +60,12 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.WriteLeafRawLines(obj); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); - renderer.WriteLine(obj.Newline); - renderer.Write(obj.AfterWhitespace); + if (!string.IsNullOrEmpty(closing)) + { + // See example 207: "> ```\nfoo\n```" + renderer.WriteLine(obj.Newline); + } + //renderer.Write(obj.AfterWhitespace); // TODO: RTP: the spec is unclear about whitespace after closing fence } else { From a04aa8a4de8499a1593567d1d00d4bbb643f6ded Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 17:50:07 +0200 Subject: [PATCH 051/120] fix empty QuoteBlock lines --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 1 + .../RoundtripSpecs/TestQuoteBlock.cs | 15 ++++++++ src/Markdig/Parsers/BlockProcessor.cs | 4 +++ src/Markdig/Parsers/MarkdownParser.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 28 +++++++++------ .../Renderers/Normalize/ParagraphRenderer.cs | 2 +- .../Renderers/Normalize/QuoteBlockRenderer.cs | 35 +++++++++++++++++-- 7 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index dad7b4891..56ea8602f 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -63,3 +63,4 @@ In order: - Newline struct itself - handling newlines - should newlines be supported? +- Example 207, 209: Special-casing certain edgecases \ No newline at end of file diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index ffa46c061..3ad24f008 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -266,5 +266,20 @@ public void TestNewline(string value) { RoundTrip(value); } + + [TestCase(">\n>q")] + [TestCase(">\n>\n>q")] + [TestCase(">q\n>\n>q")] + [TestCase(">q\n>\n>\n>q")] + [TestCase(">q\n> \n>q")] + [TestCase(">q\n> \n>q")] + [TestCase(">q\n> \n>q")] + [TestCase(">q\n>\t\n>q")] + [TestCase(">q\n>\v\n>q")] + [TestCase(">q\n>\f\n>q")] + public void TestEmptyLines(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 2c0f25167..01e06516e 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -555,6 +555,10 @@ internal void CloseAll(bool force) { break; } + if (BeforeLines != null && BeforeLines.Count > 0) + { + block.LinesAfter = UseLinesBefore(); + } Close(i); } UpdateLastBlockAndContainer(); diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 240a4c68c..c69aa0652 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -117,7 +117,7 @@ private void ProcessBlocks() if (!lineReader.IsEndOfFile()) { var lastBlock = blockProcessor.LastBlock; - if (lastBlock != null) + if (lastBlock != null && blockProcessor.BeforeLines != null) { lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); } diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index d484fbe53..8bb2066c3 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -36,13 +36,6 @@ public override BlockState TryOpen(BlockProcessor processor) var quoteChar = processor.CurrentChar; var column = processor.Column; var c = processor.NextChar(); - //if (c.IsSpaceOrTab()) - //{ - // processor.NextColumn(); - // hasSpaceAfterQuoteChar = true; - // whitespaceToAdd += 1; - //} - //beforeWhitespace.End -= 1 + (hasSpaceAfterQuoteChar ? 1 : 0); var quoteBlock = new QuoteBlock(this) { @@ -51,9 +44,16 @@ public override BlockState TryOpen(BlockProcessor processor) Span = new SourceSpan(sourcePosition, processor.Line.End), LinesBefore = processor.UseLinesBefore() }; + StringSlice afterWhitespace = StringSlice.Empty; + if (processor.Line.IsEmptyOrWhitespace()) + { + processor.Line.TrimStart(); + afterWhitespace = new StringSlice(processor.Line.Text, sourcePosition + 1, processor.Line.End); + } quoteBlock.QuoteLines.Add(new QuoteBlock.QuoteLine { BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), + AfterWhitespace = afterWhitespace, QuoteChar = true, Newline = processor.Line.Newline, }); @@ -70,6 +70,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) } var quote = (QuoteBlock) block; + var sourcePosition = processor.Start; // 5.1 Block quotes // A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space. @@ -85,20 +86,27 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) quote.QuoteLines.Add(new QuoteBlock.QuoteLine { QuoteChar = false, - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), Newline = processor.Line.Newline, }); return BlockState.None; } } + processor.NextChar(); // Skip quote marker char + StringSlice afterWhitespace = StringSlice.Empty; + if (processor.Line.IsEmptyOrWhitespace()) + { + processor.Line.TrimStart(); + afterWhitespace = new StringSlice(processor.Line.Text, sourcePosition + 1, processor.Line.End); + } quote.QuoteLines.Add(new QuoteBlock.QuoteLine { QuoteChar = true, - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), + AfterWhitespace = afterWhitespace, Newline = processor.Line.Newline, }); - processor.NextChar(); // Skip quote marker char processor.WhitespaceStart = processor.Start; block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 88e17866e..5b5fc7526 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -19,7 +19,7 @@ protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragra renderer.RenderLinesBefore(paragraph); renderer.Write(paragraph.BeforeWhitespace); renderer.WriteLeafInline(paragraph); - //renderer.Write(paragraph.Newline); + //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes renderer.RenderLinesAfter(paragraph); } } diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 02612a2d0..373d2373e 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; +using Markdig.Syntax.Inlines; using System.Collections.Generic; namespace Markdig.Renderers.Normalize @@ -21,14 +22,44 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) var indents = new List(); foreach (var quoteLine in quoteBlock.QuoteLines) { - indents.Add(quoteLine.BeforeWhitespace.ToString() + (quoteLine.QuoteChar ? ">" : "") + quoteLine.AfterWhitespace.ToString()); + var wsb = quoteLine.BeforeWhitespace.ToString(); + var quoteChar = quoteLine.QuoteChar ? ">" : ""; + var wsa = quoteLine.AfterWhitespace.ToString(); + indents.Add(wsb + quoteChar + wsa); + } + bool noChildren = false; + if (quoteBlock.Count == 0) + { + noChildren = true; + // since this QuoteBlock instance has no children, indents will not be rendered. We + // work around this by adding empty LineBreakInlines to a ParagraphBlock. + // Wanted: a more elegant/better solution (although this is not *that* bad). + foreach (var quoteLine in quoteBlock.QuoteLines) + { + // TODO: RTP: introduce EmptyQuoteLine class deriving from LeafBlock? + var emptyLeafBlock = new ParagraphBlock + { + Newline = quoteLine.Newline + }; + var newline = new LineBreakInline + { + Newline = quoteLine.Newline + }; + var container = new ContainerInline(); + container.AppendChild(newline); + emptyLeafBlock.Inline = container; + quoteBlock.Add(emptyLeafBlock); + } } renderer.PushIndent(indents); renderer.WriteChildren(quoteBlock); renderer.PopIndent(); - renderer.RenderLinesAfter(quoteBlock); + if (!noChildren) + { + renderer.RenderLinesAfter(quoteBlock); + } } } } \ No newline at end of file From fb8162f5d79d506d2d1c989ab37f575e1d394f32 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 18:28:28 +0200 Subject: [PATCH 052/120] fix whitespace surrounding closing fences of FencedCodeBlock --- .../RoundtripSpecs/TestFencedCodeBlock.cs | 5 +++++ .../CustomContainers/CustomContainer.cs | 1 + src/Markdig/Parsers/FencedBlockParserBase.cs | 6 +++++- src/Markdig/Parsers/FencedCodeBlockParser.cs | 17 ++++++++++------- .../Renderers/Normalize/CodeBlockRenderer.cs | 6 +++++- src/Markdig/Syntax/FencedCodeBlock.cs | 3 +++ src/Markdig/Syntax/IFencedBlock.cs | 2 ++ 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index 564d554bc..317dd9014 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -25,6 +25,11 @@ public class TestFencedCodeBlock [TestCase("```\n c\n```")] [TestCase("```\nc \n```")] [TestCase("```\n c \n```")] + + [TestCase(" ``` \n c \n ``` ")] + [TestCase("\t```\t\n\tc\t\n\t```\t")] + [TestCase("\v```\v\n\vc\v\n\v```\v")] + [TestCase("\f```\f\n\fc\f\n\f```\f")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index a5f353bde..fb67248e3 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -39,5 +39,6 @@ public CustomContainer(BlockParser parser) : base(parser) public int ClosingFencedCharCount { get; set; } public Newline InfoNewline { get; set; } + public StringSlice WhitespaceBeforeClosingFence { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index d78bb090c..986b5002a 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -309,20 +309,24 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) // Match if we have a closing fence var line = processor.Line; + var sourcePosition = processor.Start; var closingCount = line.CountAndSkipChar(fence.FencedChar); var diff = openingCount - closingCount; char c = line.CurrentChar; + var lastFenceCharPosition = processor.Start + closingCount; // If we have a closing fence, close it and discard the current line // The line must contain only fence opening character followed only by whitespaces. - if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd()) + if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace())) // && line.TrimEnd()) TODO: RTP { block.UpdateSpanEnd(line.Start - 1); var fencedBlock = block as IFencedBlock; fencedBlock.ClosingFencedCharCount = closingCount; fencedBlock.Newline = processor.Line.Newline; + fencedBlock.WhitespaceBeforeClosingFence = processor.PopBeforeWhitespace(sourcePosition - 1); + fencedBlock.AfterWhitespace = new StringSlice(processor.Line.Text, lastFenceCharPosition, line.End); // Don't keep the last line return BlockState.BreakDiscard; diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index d215e29d9..8daed01e2 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -41,14 +41,17 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var result = base.TryContinue(processor, block); if (result == BlockState.Continue) { - var fence = (FencedCodeBlock)block; - // Remove any indent spaces - var c = processor.CurrentChar; - var indentCount = fence.IndentCount; - while (indentCount > 0 && c.IsSpace()) + if (!processor.TrackTrivia) { - indentCount--; - c = processor.NextChar(); + var fence = (FencedCodeBlock)block; + // Remove any indent spaces + var c = processor.CurrentChar; + var indentCount = fence.IndentCount; + while (indentCount > 0 && c.IsSpace()) + { + indentCount--; + c = processor.NextChar(); + } } } diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 754cd41b2..e7881a86e 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -58,6 +58,8 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.WriteLine(fencedCodeBlock.InfoNewline); renderer.WriteLeafRawLines(obj); + + renderer.Write(fencedCodeBlock.WhitespaceBeforeClosingFence); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); if (!string.IsNullOrEmpty(closing)) @@ -65,7 +67,9 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) // See example 207: "> ```\nfoo\n```" renderer.WriteLine(obj.Newline); } - //renderer.Write(obj.AfterWhitespace); // TODO: RTP: the spec is unclear about whitespace after closing fence + // TODO: RTP: the spec is unclear about whitespace after closing fence + // imlementations seem to accept it though + renderer.Write(obj.AfterWhitespace); } else { diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index 66014bf86..682f1c459 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -53,6 +53,8 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// public int ClosingFencedCharCount { get; set; } + public StringSlice WhitespaceBeforeClosingFence { get; set; } + /// /// Gets or sets the fenced character used to open and close this fenced code block. /// @@ -60,6 +62,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// public string WhitespaceAfterFencedChar { get; set; } + public Newline InfoNewline { get; set; } /// diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 0dd65a8a7..931b8cdfa 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -50,6 +50,8 @@ public interface IFencedBlock : IBlock /// Gets or sets the fenced character count used to open this fenced code block. /// public int ClosingFencedCharCount { get; set; } + public StringSlice WhitespaceBeforeClosingFence { get; set; } + public StringSlice AfterWhitespace { get; set; } /// /// Gets or sets the fenced character used to open and close this fenced code block. From 87269d88b9071ae140585e971e141b7098e85fc8 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 18:52:48 +0200 Subject: [PATCH 053/120] fix newlines after LinkReferenceDefinition --- src/Markdig/Parsers/ParagraphBlockParser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index b27ea7270..293146443 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -234,6 +234,8 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou lrd.AfterWhitespace = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); lrd.LinesBefore = paragraph.LinesBefore; + state.BeforeLines = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack + lines = iterator.Remaining(); } else From 55eaadce67eaf2c91a1a2f77f5da1e4700b77a85 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 19:29:12 +0200 Subject: [PATCH 054/120] fix optional space after Atx heading leaders --- src/Markdig/Helpers/StringSlice.cs | 17 +++++++++++++++++ src/Markdig/Parsers/HeadingBlockParser.cs | 15 ++++++++++++++- .../Renderers/Normalize/HeadingRenderer.cs | 3 ++- src/Markdig/Syntax/HeadingBlock.cs | 3 +++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index bc941bc60..2614e26e7 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -482,5 +482,22 @@ public readonly bool IsEmptyOrWhitespace() } return true; } + + public bool Overlaps(StringSlice other) + { + if (Length == 0 || other.Length == 0) + { + return false; + } + if (other.End >= Start) + { + return true; + } + if (other.Start <= End) + { + return true; + } + return false; + } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index fa545fb66..b055c74ba 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -72,10 +72,16 @@ public override BlockState TryOpen(BlockProcessor processor) // A space is required after leading # if (leadingCount > 0 && leadingCount <= MaxLeadingCount && (c.IsSpaceOrTab() || c == '\0')) { + StringSlice whitespace = StringSlice.Empty; + if (c.IsSpaceOrTab()) + { + whitespace = new StringSlice(processor.Line.Text, processor.Start + leadingCount, processor.Start + leadingCount); + } // Move to the content var headingBlock = new HeadingBlock(this) { HeaderChar = matchingChar, + WhitespaceAfterAtxHeaderChar = whitespace, Level = leadingCount, Column = column, Span = { Start = sourcePosition }, @@ -129,7 +135,14 @@ public override BlockState TryOpen(BlockProcessor processor) // Setup the source end position of this element headingBlock.Span.End = processor.Line.End; - headingBlock.AfterWhitespace = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); + + var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); + headingBlock.AfterWhitespace = wsa; + if (wsa.Overlaps(headingBlock.WhitespaceAfterAtxHeaderChar)) + { + // prevent double whitespace allocation in case of closing # i.e. "# #" + headingBlock.WhitespaceAfterAtxHeaderChar = StringSlice.Empty; + } // We expect a single line, so don't continue return BlockState.Break; diff --git a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs index 39c6c0240..85978cbd8 100644 --- a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs @@ -48,7 +48,8 @@ protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) : new string('#', obj.Level); renderer.Write(obj.BeforeWhitespace); - renderer.Write(headingText).Write(' '); + renderer.Write(headingText); + renderer.Write(obj.WhitespaceAfterAtxHeaderChar); renderer.WriteLeafInline(obj); renderer.Write(obj.AfterWhitespace); renderer.WriteLine(obj.Newline); diff --git a/src/Markdig/Syntax/HeadingBlock.cs b/src/Markdig/Syntax/HeadingBlock.cs index ccd593159..1ddf609e9 100644 --- a/src/Markdig/Syntax/HeadingBlock.cs +++ b/src/Markdig/Syntax/HeadingBlock.cs @@ -38,5 +38,8 @@ public HeadingBlock(BlockParser parser) : base(parser) public int HeaderCharCount { get; set; } public Newline SetextNewline { get; set; } + + // space is mandatory after # only when there is heading content. I.e. "#" is valid + public StringSlice WhitespaceAfterAtxHeaderChar { get; set; } } } \ No newline at end of file From 7b20299d2b27796c886dfb0b679149c2ee526a07 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 19:34:16 +0200 Subject: [PATCH 055/120] fix empty ListItem --- src/Markdig/Renderers/Normalize/ListRenderer.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 286704588..c9b062671 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -76,7 +76,14 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) StringSlice aws = listItem.AfterWhitespace; renderer.PushIndent(new List { $@"{bws}{bullet}{aws}" }); - renderer.WriteChildren(listItem); + if (listItem.Count == 0) + { + renderer.Write(""); // trigger writing of indent + } + else + { + renderer.WriteChildren(listItem); + } renderer.PopIndent(); renderer.RenderLinesAfter(listItem); From 6506e4594c399da8e6304d17bc0f67ee40b1cb27 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 22:47:39 +0200 Subject: [PATCH 056/120] fix escaped characters in LinkInline --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 2 +- src/Markdig/Helpers/LinkHelper.cs | 259 +++++++++++++++++- .../Parsers/Inlines/LinkInlineParser.cs | 4 + .../Normalize/Inlines/LinkInlineRenderer.cs | 7 +- src/Markdig/Syntax/Inlines/LinkInline.cs | 2 + 5 files changed, 268 insertions(+), 6 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 56ea8602f..49828ff31 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -63,4 +63,4 @@ In order: - Newline struct itself - handling newlines - should newlines be supported? -- Example 207, 209: Special-casing certain edgecases \ No newline at end of file +- Example 207, 209, 291: Special-casing certain edgecases \ No newline at end of file diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 8dbabfabb..6c864b5b7 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -418,7 +418,9 @@ public static bool TryParseInlineLink(ref StringSlice text, out string link, out public static bool TryParseInlineLinkWhitespace( ref StringSlice text, out string link, + out string unescapedLink, out string title, + out string unescapedTitle, out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, @@ -436,7 +438,9 @@ public static bool TryParseInlineLinkWhitespace( bool isValid = false; var c = text.CurrentChar; link = null; + unescapedLink = null; title = null; + unescapedTitle = null; linkSpan = SourceSpan.Empty; titleSpan = SourceSpan.Empty; @@ -454,7 +458,7 @@ public static bool TryParseInlineLinkWhitespace( text.TrimStart(); whitespaceBeforeLink = new SourceSpan(sourcePosition, text.Start - 1); var pos = text.Start; - if (TryParseUrl(ref text, out link, out urlHasPointyBrackets)) + if (TryParseUrlWhitespace(ref text, out link, out unescapedLink, out urlHasPointyBrackets)) { linkSpan.Start = pos; linkSpan.End = text.Start - 1; @@ -482,7 +486,7 @@ public static bool TryParseInlineLinkWhitespace( { isValid = true; } - else if (TryParseTitle(ref text, out title, out titleEnclosingCharacter)) + else if (TryParseTitleWhitespace(ref text, out title, out unescapedTitle, out titleEnclosingCharacter)) { titleSpan.Start = pos; titleSpan.End = text.Start - 1; @@ -612,6 +616,108 @@ public static bool TryParseTitle(ref T text, out string title, out char enclo return isValid; } + public static bool TryParseTitleWhitespace(ref T text, out string title, out string unescapedTitle, out char enclosingCharacter) where T : ICharIterator + { + bool isValid = false; + var buffer = StringBuilderCache.Local(); + var unescaped = new StringBuilder(); + enclosingCharacter = '\0'; + + // a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or + // a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or + var c = text.CurrentChar; + if (c == '\'' || c == '"' || c == '(') + { + enclosingCharacter = c; + var closingQuote = c == '(' ? ')' : c; + bool hasEscape = false; + // -1: undefined + // 0: has only spaces + // 1: has other characters + int hasOnlyWhiteSpacesSinceLastLine = -1; + while (true) + { + c = text.NextChar(); + + if (c == '\r' || c == '\n') + { + if (hasOnlyWhiteSpacesSinceLastLine >= 0) + { + if (hasOnlyWhiteSpacesSinceLastLine == 1) + { + break; + } + hasOnlyWhiteSpacesSinceLastLine = -1; + } + buffer.Append(c); + unescaped.Append(c); + if (c == '\r' && text.PeekChar() == '\n') + { + buffer.Append('\n'); + unescaped.Append(c); + } + continue; + } + + if (c == '\0') + { + break; + } + + if (c == closingQuote) + { + if (hasEscape) + { + buffer.Append(closingQuote); + unescaped.Append(closingQuote); + hasEscape = false; + continue; + } + + // Skip last quote + text.NextChar(); + isValid = true; + break; + } + + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + unescaped.Append('\\'); + } + + if (c == '\\') + { + hasEscape = true; + unescaped.Append('\\'); + continue; + } + + hasEscape = false; + + if (c.IsSpaceOrTab()) + { + if (hasOnlyWhiteSpacesSinceLastLine < 0) + { + hasOnlyWhiteSpacesSinceLastLine = 1; + } + } + else if (c != '\n' && c != '\r' && (c != '\r' && text.PeekChar() != '\n')) + { + hasOnlyWhiteSpacesSinceLastLine = 0; + } + + buffer.Append(c); + unescaped.Append(c); + } + } + + title = isValid ? buffer.ToString() : null; + unescapedTitle = isValid ? unescaped.ToString() : null; + buffer.Length = 0; + return isValid; + } + public static bool TryParseUrl(T text, out string link) where T : ICharIterator { return TryParseUrl(ref text, out link, out _); @@ -757,6 +863,155 @@ public static bool TryParseUrl(ref T text, out string link, out bool hasPoint return isValid; } + public static bool TryParseUrlWhitespace(ref T text, out string link, out string unescapedLink, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator + { + bool isValid = false; + hasPointyBrackets = false; + var buffer = StringBuilderCache.Local(); + var unescaped = new StringBuilder(); + unescapedLink = null; + + var c = text.CurrentChar; + + // a sequence of zero or more characters between an opening < and a closing > + // that contains no line breaks, or unescaped < or > characters, or + if (c == '<') + { + bool hasEscape = false; + do + { + c = text.NextChar(); + if (!hasEscape && c == '>') + { + text.NextChar(); + hasPointyBrackets = true; + isValid = true; + break; + } + + if (!hasEscape && c == '<') + { + break; + } + + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + unescaped.Append('\\'); + } + + if (c == '\\') + { + hasEscape = true; + unescaped.Append('\\'); + continue; + } + + if (c.IsNewLine()) + { + break; + } + + hasEscape = false; + + buffer.Append(c); + unescaped.Append(c); + + } while (c != '\0'); + } + else + { + // a nonempty sequence of characters that does not start with <, does not include ASCII space or control characters, + // and includes parentheses only if (a) they are backslash-escaped or (b) they are part of a + // balanced pair of unescaped parentheses that is not itself inside a balanced pair of unescaped + // parentheses. + bool hasEscape = false; + int openedParent = 0; + while (true) + { + // Match opening and closing parenthesis + if (c == '(') + { + if (!hasEscape) + { + openedParent++; + } + } + + if (c == ')') + { + if (!hasEscape) + { + openedParent--; + if (openedParent < 0) + { + isValid = true; + break; + } + } + } + + if (!isAutoLink) + { + if (hasEscape && !c.IsAsciiPunctuation()) + { + buffer.Append('\\'); + unescaped.Append('\\'); + } + + // If we have an escape + if (c == '\\') + { + hasEscape = true; + c = text.NextChar(); + unescaped.Append('\\'); + continue; + } + + hasEscape = false; + } + + if (IsEndOfUri(c, isAutoLink)) + { + isValid = true; + break; + } + + if (isAutoLink) + { + if (c == '&') + { + if (HtmlHelper.ScanEntity(text, out _, out _, out _) > 0) + { + isValid = true; + break; + } + } + if (IsTrailingUrlStopCharacter(c) && IsEndOfUri(text.PeekChar(), true)) + { + isValid = true; + break; + } + } + + buffer.Append(c); + unescaped.Append(c); + + c = text.NextChar(); + } + + if (openedParent > 0) + { + isValid = false; + } + } + + link = isValid ? buffer.ToString() : null; + unescapedLink = isValid ? unescaped.ToString() : null; + buffer.Length = 0; + return isValid; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsTrailingUrlStopCharacter(char c) { diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index a6f46a4ee..eb12a28f9 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -223,7 +223,9 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice if (LinkHelper.TryParseInlineLinkWhitespace( ref text, out string url, + out string unescapedUrl, out string title, + out string unescapedTitle, out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, @@ -240,9 +242,11 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice { WhitespaceBeforeUrl = wsBeforeLink, Url = HtmlHelper.Unescape(url), // TODO: RTP: unescape + UnescapedUrl = unescapedUrl, // TODO: RTP: unescape UrlHasPointyBrackets = urlHasPointyBrackets, WhitespaceAfterUrl = wsAfterLink, Title = HtmlHelper.Unescape(title), // TODO: RTP: unescape + UnescapedTitle = unescapedTitle, TitleEnclosingCharacter = titleEnclosingCharacter, WhitespaceAfterTitle = wsAfterTitle, IsImage = openParent.IsImage, diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index 723a3dced..b191a429b 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -58,14 +58,14 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) { renderer.Write('<'); } - renderer.Write(link.Url); + renderer.Write(link.UnescapedUrl); if (link.UrlHasPointyBrackets) { renderer.Write('>'); } renderer.Write(link.WhitespaceAfterUrl); - if (!string.IsNullOrEmpty(link.Title)) + if (!string.IsNullOrEmpty(link.UnescapedTitle)) { var open = link.TitleEnclosingCharacter; var close = link.TitleEnclosingCharacter; @@ -74,7 +74,8 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) close = ')'; } renderer.Write(open); - renderer.Write(link.Title.Replace(@"""", @"\""")); // TODO: RTP: should this always be done? + //renderer.Write(link.Title.Replace(@"""", @"\""")); // TODO: RTP: should this always be done? + renderer.Write(link.UnescapedTitle); renderer.Write(close); renderer.Write(link.WhitespaceAfterTitle); } diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index fbce61640..7262f9c84 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -45,6 +45,7 @@ public LinkInline(string url, string title) /// Gets or sets the URL. /// public string Url { get; set; } + public string UnescapedUrl { get; internal set; } public bool UrlHasPointyBrackets { get; set; } @@ -60,6 +61,7 @@ public LinkInline(string url, string title) /// Gets or sets the title. /// public string Title { get; set; } + public string UnescapedTitle { get; internal set; } public char TitleEnclosingCharacter { get; set; } From 7a9405ec9e2bf2cb5b10b40ebea107c4de1f8b3a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 22:55:55 +0200 Subject: [PATCH 057/120] fix ThematicBreakParser --- src/Markdig/Parsers/ThematicBreakParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index 0fa5b1468..de9d6f9f6 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -95,7 +95,7 @@ public override BlockState TryOpen(BlockProcessor processor) //BeforeWhitespace = beforeWhitespace, //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), LinesBefore = processor.UseLinesBefore(), - Content = new StringSlice(line.Text, processor.CurrentLineStartPosition, line.End, line.Newline), + Content = new StringSlice(line.Text, processor.WhitespaceStart, line.End, line.Newline), //include whitespace for now Newline = processor.Line.Newline, }); return BlockState.BreakDiscard; From 8c37a1bef506e495f7e32a277c5ce64d05bfd2e5 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 23:00:38 +0200 Subject: [PATCH 058/120] fix HtmlBlock whitespace before --- src/Markdig/Parsers/HtmlBlockParser.cs | 1 + src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Markdig/Parsers/HtmlBlockParser.cs b/src/Markdig/Parsers/HtmlBlockParser.cs index 97d490842..52cb7841b 100644 --- a/src/Markdig/Parsers/HtmlBlockParser.cs +++ b/src/Markdig/Parsers/HtmlBlockParser.cs @@ -277,6 +277,7 @@ private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int Type = type, // By default, setup to the end of line Span = new SourceSpan(startPosition, startPosition + state.Line.End), + BeforeWhitespace = state.PopBeforeWhitespace(startPosition - 1), LinesBefore = state.UseLinesBefore(), Newline = state.Line.Newline, }); diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index fd0773a5a..236bd53cf 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -11,6 +11,7 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { renderer.RenderLinesBefore(obj); + renderer.Write(obj.BeforeWhitespace); renderer.WriteLeafRawLines(obj); renderer.RenderLinesAfter(obj); } From b00d75607b25517c81e3f9746146fd32e2d432b5 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 23:21:11 +0200 Subject: [PATCH 059/120] fix tabs in IndentedCodeBlock --- .../DefinitionLists/DefinitionListParser.cs | 2 +- .../Extensions/Figures/FigureBlockParser.cs | 4 ++-- .../Extensions/Tables/PipeTableBlockParser.cs | 4 ++-- src/Markdig/Parsers/BlockProcessor.cs | 6 +++--- src/Markdig/Syntax/LeafBlock.cs | 16 +++++++++++----- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs index f8460ad44..7534f77cb 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs @@ -90,7 +90,7 @@ public override BlockState TryOpen(BlockProcessor processor) Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End), IsOpen = false }; - term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position); + term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, false); definitionItem.Add(term); } currentDefinitionList.Add(definitionItem); diff --git a/src/Markdig/Extensions/Figures/FigureBlockParser.cs b/src/Markdig/Extensions/Figures/FigureBlockParser.cs index 2b777d3c4..4f075e800 100644 --- a/src/Markdig/Extensions/Figures/FigureBlockParser.cs +++ b/src/Markdig/Extensions/Figures/FigureBlockParser.cs @@ -61,7 +61,7 @@ public override BlockState TryOpen(BlockProcessor processor) Column = column + line.Start - startPosition, IsOpen = false }; - caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition); + caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, false); figure.Add(caption); } processor.NewBlocks.Push(figure); @@ -96,7 +96,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) Column = column + line.Start - startPosition, IsOpen = false }; - caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition); + caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, false); figure.Add(caption); } diff --git a/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs b/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs index 3073610df..04af5d207 100644 --- a/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs +++ b/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// Copyright (c) Alexandre Mutel. All rights reserved. // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. @@ -45,7 +45,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (countPipe > 0) { // Mark the paragraph as open (important, otherwise we would have an infinite loop) - paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start); + paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start, false); paragraph.IsOpen = true; return BlockState.BreakDiscard; } diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 01e06516e..593bcee00 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -672,7 +672,7 @@ private void TryContinueBlocks() { //UnwindAllIndents(); } - leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); + leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } } @@ -811,7 +811,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) { UnwindAllIndents(); } - paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); + paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } // TODO: RTP: delegate this to container parser classes var qb = paragraph.Parent as QuoteBlock; @@ -875,7 +875,7 @@ private void ProcessNewBlocks(BlockState result, bool allowClosing) { UnwindAllIndents(); } - leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition); + leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } if (newBlocks.Count > 0) diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index 7133c80e3..fb59d370d 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -48,7 +48,7 @@ protected LeafBlock(BlockParser parser) : base(parser) /// The column. /// The line. /// - public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition) + public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition, bool preserveTrivia) { if (Lines.Lines == null) { @@ -68,11 +68,17 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi } else { - // We need to expand tabs to spaces - // TODO: RTP: also register tabs here var builder = StringBuilderCache.Local(); - builder.Append(' ', CharHelper.AddTab(column) - column); - builder.Append(slice.Text, slice.Start + 1, slice.Length - 1); + if (preserveTrivia) + { + builder.Append(slice.Text, slice.Start, slice.Length); + } + else + { + // We need to expand tabs to spaces + builder.Append(' ', CharHelper.AddTab(column) - column); + builder.Append(slice.Text, slice.Start + 1, slice.Length - 1); + } stringLine.Slice = new StringSlice(builder.GetStringAndReset()); Lines.Add(ref stringLine); } From de5526877dafa45e64b54ec7b7f65267a3e6aef4 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 23 Oct 2020 23:31:31 +0200 Subject: [PATCH 060/120] fix broken AtxHeading whitespace --- src/Markdig/Helpers/StringSlice.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 2614e26e7..e77c13c90 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -489,11 +489,11 @@ public bool Overlaps(StringSlice other) { return false; } - if (other.End >= Start) + if (Start <= other.End) { return true; } - if (other.Start <= End) + if (End <= other.Start) { return true; } From ef343158b7b5741779c4e9be71c9415fd7226ba6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 00:11:35 +0200 Subject: [PATCH 061/120] fix escapes in LinkReferenceDefinition --- src/Markdig/Helpers/LinkHelper.cs | 8 ++++++-- .../Normalize/LinkReferenceDefinitionRenderer.cs | 4 ++-- src/Markdig/Syntax/LinkReferenceDefinition.cs | 6 ++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 6c864b5b7..4adf78870 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -1183,9 +1183,11 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( out string labelWithWhitespace, out SourceSpan whitespaceBeforeUrl, // can contain newline out string url, + out string unescapedUrl, out bool urlHasPointyBrackets, out SourceSpan whitespaceBeforeTitle, // can contain newline out string title, // can contain non-consecutive newlines + out string unescapedTitle, out char titleEnclosingCharacter, out SourceSpan whitespaceAfterTitle, out SourceSpan labelSpan, @@ -1194,8 +1196,10 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( { whitespaceBeforeUrl = SourceSpan.Empty; url = null; + unescapedUrl = null; whitespaceBeforeTitle = SourceSpan.Empty; title = null; + unescapedTitle = null; urlSpan = SourceSpan.Empty; titleSpan = SourceSpan.Empty; @@ -1225,7 +1229,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( urlSpan.Start = text.Start; bool isAngleBracketsUrl = text.CurrentChar == '<'; - if (!TryParseUrl(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + if (!TryParseUrlWhitespace(ref text, out url, out unescapedUrl, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) { return false; } @@ -1240,7 +1244,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( if (c == '\'' || c == '"' || c == '(') { titleSpan.Start = text.Start; - if (TryParseTitle(ref text, out title, out titleEnclosingCharacter)) + if (TryParseTitleWhitespace(ref text, out title, out unescapedTitle, out titleEnclosingCharacter)) { titleSpan.End = text.Start - 1; // If we have a title, it requires a whitespace after the url diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs index cf269c111..132f52efe 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs @@ -22,7 +22,7 @@ protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitio { renderer.Write('<'); } - renderer.Write(linkDef.Url); + renderer.Write(linkDef.UnescapedUrl); if (linkDef.UrlHasPointyBrackets) { renderer.Write('>'); @@ -38,7 +38,7 @@ protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitio close = ')'; } renderer.Write(open); - renderer.Write(linkDef.Title.Replace("\"", "\\\"")); // TODO: RTP: should this always be done? + renderer.Write(linkDef.UnescapedTitle); renderer.Write(close); } renderer.Write(linkDef.AfterWhitespace); diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 9b430934d..8c4ddb8e8 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -61,6 +61,7 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// Gets or sets the URL. /// public string Url { get; set; } + public string UnescapedUrl { get; set; } public bool UrlHasPointyBrackets { get; set; } @@ -70,6 +71,7 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// Gets or sets the title. /// public string Title { get; set; } + public string UnescapedTitle { get; set; } public char TitleEnclosingCharacter { get; set; } @@ -151,9 +153,11 @@ public static bool TryParseWhitespace( out string labelWithWhitespace, out whitespaceBeforeUrl, out string url, + out string unescapedUrl, out bool urlHasPointyBrackets, out whitespaceBeforeTitle, out string title, + out string unescapedTitle, out char titleEnclosingCharacter, out whitespaceAfterTitle, out SourceSpan labelSpan, @@ -170,6 +174,8 @@ public static bool TryParseWhitespace( LabelWithWhitespace = labelWithWhitespace, LabelSpan = labelSpan, UrlSpan = urlSpan, + UnescapedUrl = unescapedUrl, + UnescapedTitle = unescapedTitle, TitleSpan = titleSpan, Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End) }; From 9beb0c1613c6c15808c304b90711cdf56f76b6c2 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 00:24:00 +0200 Subject: [PATCH 062/120] fix whitespace on empty lines --- src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs | 8 ++++++++ src/Markdig/Parsers/BlockProcessor.cs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index 6e38b9ad7..9dc612641 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -210,6 +210,14 @@ public void TestNewline(string value) [TestCase(" \np")] [TestCase(" \rp")] [TestCase(" \r\np")] + + [TestCase(" \np")] + [TestCase(" \rp")] + [TestCase(" \r\np")] + + [TestCase(" \np")] + [TestCase(" \rp")] + [TestCase(" \r\np")] public void Test_WhitespaceWithNewline(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 593bcee00..0f03d7570 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -760,10 +760,10 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (Line.IsEmpty) { BeforeLines ??= new List(); - Line.Start = StartBeforeIndent; var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); BeforeLines.Add(line); ContinueProcessingLine = false; + Line.Start = StartBeforeIndent; break; } From db2856daf7acdb6f1c657b961ab285563b513fbe Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 13:53:29 +0200 Subject: [PATCH 063/120] fix label rendering on LinkInline --- .../RoundtripSpecs/Inlines/TestLinkInline.cs | 1 + .../RoundtripSpecs/TestAtxHeading.cs | 4 +++ .../RoundtripSpecs/TestIndentedCodeBlock.cs | 1 + .../RoundtripSpecs/TestParagraph.cs | 16 ++++++++++++ src/Markdig/Parsers/BlockProcessor.cs | 3 ++- .../Parsers/Inlines/LinkInlineParser.cs | 19 +++++++++++--- .../Normalize/Inlines/LinkInlineRenderer.cs | 25 +++++-------------- src/Markdig/Syntax/Inlines/LinkInline.cs | 10 ++++++++ 8 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs index 479d80768..4322075d0 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -204,6 +204,7 @@ public void Test_PointyBrackets(string value) [TestCase("[*a*][a]")] [TestCase("[a][b]")] [TestCase("[a][]")] + [TestCase("[a]")] public void Test_Inlines(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs index 5442d95d7..4b6607952 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestAtxHeading.cs @@ -43,6 +43,10 @@ public void TestParagraph(string value) [TestCase("\r\n# h\n")] [TestCase("\r\n# h\r")] [TestCase("\r\n# h\r\n")] + + [TestCase("# h\n\n ")] + [TestCase("# h\n\n ")] + [TestCase("# h\n\n ")] public void TestNewline(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index 8912636a9..ca746fd06 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -11,6 +11,7 @@ public class TestIndentedCodeBlock [TestCase(" l")] [TestCase(" l")] [TestCase("\tl")] + [TestCase("\t\tl")] [TestCase("\tl1\n l1")] [TestCase("\n l")] diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index 9dc612641..28855e3a4 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -218,6 +218,22 @@ public void TestNewline(string value) [TestCase(" \np")] [TestCase(" \rp")] [TestCase(" \r\np")] + + [TestCase(" \n ")] + [TestCase(" \r ")] + [TestCase(" \r\n ")] + + [TestCase(" \np ")] + [TestCase(" \rp ")] + [TestCase(" \r\np ")] + + [TestCase(" \np ")] + [TestCase(" \rp ")] + [TestCase(" \r\np ")] + + [TestCase(" \np ")] + [TestCase(" \rp ")] + [TestCase(" \r\np ")] public void Test_WhitespaceWithNewline(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 0f03d7570..690fd25b1 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -685,8 +685,9 @@ private void TryContinueBlocks() if (Line.IsEmpty) { BeforeLines ??= new List(); + var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); + BeforeLines.Add(line); Line.Start = StartBeforeIndent; - BeforeLines.Add(Line); } ContinueProcessingLine = false; break; diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index eb12a28f9..55ee01e52 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -109,7 +109,14 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) return false; } - private bool ProcessLinkReference(InlineProcessor state, string label, bool isShortcut, SourceSpan labelSpan, LinkDelimiterInline parent, int endPosition) + private bool ProcessLinkReference( + InlineProcessor state, + string label, + bool isShortcut, + SourceSpan labelSpan, + LinkDelimiterInline parent, + int endPosition, + LocalLabel localLabel) { if (!state.Document.TryGetLinkReferenceDefinition(label, out LinkReferenceDefinition linkRef)) { @@ -133,7 +140,10 @@ private bool ProcessLinkReference(InlineProcessor state, string label, bool isSh Title = HtmlHelper.Unescape(linkRef.Title), Label = label, LabelSpan = labelSpan, - LabelWithWhitespace = linkRef.LabelWithWhitespace, + LabelWithWhitespace = linkRef.LabelWithWhitespace, // TODO + LinkRefDefLabel = linkRef.Label, + LinkRefDefLabelWithWhitespace = linkRef.LabelWithWhitespace, + LocalLabel = localLabel, UrlSpan = linkRef.UrlSpan, IsImage = parent.IsImage, IsShortcut = isShortcut, @@ -325,6 +335,7 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice bool isLabelSpanLocal = true; bool isShortcut = false; + LocalLabel localLabel = LocalLabel.Local; // Handle Collapsed links if (text.CurrentChar == '[') { @@ -333,12 +344,14 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice label = openParent.Label; labelSpan = openParent.LabelSpan; isLabelSpanLocal = false; + localLabel = LocalLabel.Empty; text.NextChar(); // Skip [ text.NextChar(); // Skip ] } } else { + localLabel = LocalLabel.None; label = openParent.Label; isShortcut = true; } @@ -350,7 +363,7 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); } - if (ProcessLinkReference(inlineState, label, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1))) + if (ProcessLinkReference(inlineState, label, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) { // Remove the open parent openParent.Remove(); diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index b191a429b..ca31273c6 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -14,38 +14,25 @@ public class LinkInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, LinkInline link) { - //foreach (var child in link) - //{ - // if (child is LiteralInline li) - // { - // renderer.Write(li.Content.Text); - // } - //} - //return; if (link.IsImage) { renderer.Write('!'); } + // link text renderer.Write('['); renderer.WriteChildren(link); renderer.Write(']'); if (link.Label != null) { - if (link.FirstChild is LiteralInline literal && - literal.Content.Length == link.Label.Length && - literal.Content.Match(link.Label)) + if (link.LocalLabel == LocalLabel.Empty || link.LocalLabel == LocalLabel.Empty) { - // collapsed reference and shortcut links - if (!link.IsShortcut) + renderer.Write('['); + if (link.LocalLabel == LocalLabel.Local) { - renderer.Write("[]"); + renderer.Write(link.Label); } - } - else - { - // full link - renderer.Write('[').Write(link.LabelWithWhitespace).Write(']'); + renderer.Write(']'); } } else diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index 7262f9c84..8e67be8d9 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -7,6 +7,12 @@ namespace Markdig.Syntax.Inlines { + public enum LocalLabel + { + Local, // [foo][bar] + Empty, // [foo][] + None, // [foo] + } /// /// A Link inline (Section 6.5 CommonMark specs) /// @@ -72,6 +78,8 @@ public LinkInline(string url, string title) /// public string Label { get; set; } + public string LinkRefDefLabel { get; set; } + public string LabelWithWhitespace { get; set; } /// @@ -93,6 +101,8 @@ public LinkInline(string url, string title) /// Gets or sets the reference this link is attached to. May be null. /// public LinkReferenceDefinition Reference { get; set; } + public string LinkRefDefLabelWithWhitespace { get; internal set; } + public LocalLabel LocalLabel { get; internal set; } /// /// The URL source span. From 1b44256fc093b2b52868ad39cc1233dde9e07a3e Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 14:13:38 +0200 Subject: [PATCH 064/120] improve whitespace handling in FencedCodeBlock --- src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs | 10 +++++++++- src/Markdig/Parsers/FencedBlockParserBase.cs | 3 +++ src/Markdig/Parsers/HeadingBlockParser.cs | 2 +- src/Markdig/Parsers/MarkdownParser.cs | 9 +++------ src/Markdig/Renderers/Normalize/ListRenderer.cs | 1 + src/Markdig/Syntax/IBlock.cs | 5 +++++ src/Markdig/Syntax/IFencedBlock.cs | 1 - 7 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index c6a0d6327..3df63fe3f 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -104,12 +104,20 @@ public class TestOrderedList [TestCase("1. i\n2. j\n3. k")] [TestCase("1. i\n2. j\n3. k\n")] - public void Test(string value) { RoundTrip(value); } + [TestCase("10. i")] + [TestCase("11. i")] + [TestCase("10. i\n12. i")] + public void Test_MoreThenOneStart(string value) + { + RoundTrip(value); + } + + [TestCase("\n1. i")] [TestCase("\r1. i")] [TestCase("\r\n1. i")] diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 986b5002a..ea7536770 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -90,6 +90,8 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice // after args? // info: between fencedchars and + var wsb = blockProcessor.PopBeforeWhitespace(blockProcessor.Start - 1); + // An info string cannot contain any backticks (unless it is a tilde block) for (int i = line.Start; i <= line.End; i++) { @@ -166,6 +168,7 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice fenced.Arguments = HtmlHelper.Unescape(arg); fenced.WhitespaceAfterArguments = afterArg; fenced.InfoNewline = line.Newline; + fenced.BeforeWhitespace = wsb; return true; } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index b055c74ba..fc91b3903 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -84,7 +84,7 @@ public override BlockState TryOpen(BlockProcessor processor) WhitespaceAfterAtxHeaderChar = whitespace, Level = leadingCount, Column = column, - Span = { Start = sourcePosition }, + Span = { Start = sourcePosition }, BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), Newline = processor.Line.Newline, diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index c69aa0652..53889d620 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -114,13 +114,10 @@ private void ProcessBlocks() // If this is the end of file and the last line is empty if (lineText.Text is null) { - if (!lineReader.IsEndOfFile()) + var lastBlock = blockProcessor.LastBlock; + if (lastBlock != null && blockProcessor.BeforeLines != null) { - var lastBlock = blockProcessor.LastBlock; - if (lastBlock != null && blockProcessor.BeforeLines != null) - { - lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); - } + lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); } break; } diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index c9b062671..61fa6c155 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -44,6 +44,7 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) renderer.Write(listItem.BeforeWhitespace); renderer.Write(index.ToString(CultureInfo.InvariantCulture)); + //renderer.Write(listItem.Order.ToString(CultureInfo.InvariantCulture)); renderer.Write(listBlock.OrderedDelimiter); renderer.Write(' '); renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); diff --git a/src/Markdig/Syntax/IBlock.cs b/src/Markdig/Syntax/IBlock.cs index c0cd35b22..242250f4b 100644 --- a/src/Markdig/Syntax/IBlock.cs +++ b/src/Markdig/Syntax/IBlock.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; namespace Markdig.Syntax @@ -56,5 +57,9 @@ public interface IBlock : IMarkdownObject /// Occurs when the process of inlines ends for this instance. /// event ProcessInlineDelegate ProcessInlinesEnd; + + public StringSlice BeforeWhitespace { get; set; } + public StringSlice AfterWhitespace { get; set; } + } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 931b8cdfa..9dd3b167e 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -51,7 +51,6 @@ public interface IFencedBlock : IBlock /// public int ClosingFencedCharCount { get; set; } public StringSlice WhitespaceBeforeClosingFence { get; set; } - public StringSlice AfterWhitespace { get; set; } /// /// Gets or sets the fenced character used to open and close this fenced code block. From 147cd48a8df7b784ae66940bb98a3e9881747cac Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 14:29:39 +0200 Subject: [PATCH 065/120] fix escapes in InlineLink regression --- src/Markdig/Helpers/LinkHelper.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 4adf78870..e937391cb 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -683,7 +683,6 @@ public static bool TryParseTitleWhitespace(ref T text, out string title, out if (hasEscape && !c.IsAsciiPunctuation()) { buffer.Append('\\'); - unescaped.Append('\\'); } if (c == '\\') @@ -869,7 +868,6 @@ public static bool TryParseUrlWhitespace(ref T text, out string link, out str hasPointyBrackets = false; var buffer = StringBuilderCache.Local(); var unescaped = new StringBuilder(); - unescapedLink = null; var c = text.CurrentChar; @@ -956,7 +954,6 @@ public static bool TryParseUrlWhitespace(ref T text, out string link, out str if (hasEscape && !c.IsAsciiPunctuation()) { buffer.Append('\\'); - unescaped.Append('\\'); } // If we have an escape From eac0ab3ccab84469fe924be1eb42c9123030cbeb Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 14:37:49 +0200 Subject: [PATCH 066/120] fix broken InlineLink rendering --- src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index ca31273c6..167cc20bd 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -25,7 +25,7 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) if (link.Label != null) { - if (link.LocalLabel == LocalLabel.Empty || link.LocalLabel == LocalLabel.Empty) + if (link.LocalLabel == LocalLabel.Local || link.LocalLabel == LocalLabel.Empty) { renderer.Write('['); if (link.LocalLabel == LocalLabel.Local) From d7558f0442d00bc4fc022db8eff9e8d5e74f9386 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 14:41:35 +0200 Subject: [PATCH 067/120] fix FencedCodeBlock beforewhitespace regression --- src/Markdig/Parsers/FencedBlockParserBase.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index ea7536770..986b5002a 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -90,8 +90,6 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice // after args? // info: between fencedchars and - var wsb = blockProcessor.PopBeforeWhitespace(blockProcessor.Start - 1); - // An info string cannot contain any backticks (unless it is a tilde block) for (int i = line.Start; i <= line.End; i++) { @@ -168,7 +166,6 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice fenced.Arguments = HtmlHelper.Unescape(arg); fenced.WhitespaceAfterArguments = afterArg; fenced.InfoNewline = line.Newline; - fenced.BeforeWhitespace = wsb; return true; } From b0602a7bb00c3330d274dbb4a7030b5bdccb7851 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 14:51:09 +0200 Subject: [PATCH 068/120] fix HtmlBlock whitespace regression --- src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index 236bd53cf..cdeda8fcf 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -11,7 +11,7 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { renderer.RenderLinesBefore(obj); - renderer.Write(obj.BeforeWhitespace); + //renderer.Write(obj.BeforeWhitespace); // Lines content is written, including whitespace renderer.WriteLeafRawLines(obj); renderer.RenderLinesAfter(obj); } From 34e439f49405fc4020fe6925e3da1bb2343c770f Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 15:27:10 +0200 Subject: [PATCH 069/120] fix whitespace rendering of FencedCodeBlock as child of ListItemBlock --- src/Markdig/Parsers/BlockProcessor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 690fd25b1..b2567540c 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -670,7 +670,12 @@ private void TryContinueBlocks() { if (TrackTrivia) { - //UnwindAllIndents(); + if (block is FencedCodeBlock && block.Parent is ListItemBlock) + { + // the line was already given to the parent, rendering will ignore that parent line. + // The child FencedCodeBlock should get the eaten whitespace at start of the line. + UnwindAllIndents(); + } } leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } From 5a19cdfebb93a14dcf32f26dc678be014a7dd849 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 15:27:37 +0200 Subject: [PATCH 070/120] implement ordered list item bullet rendering --- src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs | 1 + src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs | 1 + src/Markdig/Parsers/ListBlockParser.cs | 1 + src/Markdig/Parsers/ListInfo.cs | 6 ++++++ src/Markdig/Parsers/NumberedListItemParser.cs | 4 ++++ src/Markdig/Renderers/Normalize/ListRenderer.cs | 4 ++-- src/Markdig/Syntax/ListItemBlock.cs | 3 +++ 7 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index 3df63fe3f..0b14fd8b3 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -112,6 +112,7 @@ public void Test(string value) [TestCase("10. i")] [TestCase("11. i")] [TestCase("10. i\n12. i")] + [TestCase("2. i\n3. i")] public void Test_MoreThenOneStart(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index 4b209b3d8..2247a8491 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -82,6 +82,7 @@ public void TestIndentedCodeBlock(string value) } [TestCase("- ```a```")] + [TestCase("- ```\n a\n```")] [TestCase("- i1\n - i1.1\n ```\n c\n ```")] [TestCase("- i1\n - i1.1\n ```\nc\n```")] [TestCase("- i1\n - i1.1\n ```\nc\n```\n")] diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 111c6cced..54e29f37d 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -274,6 +274,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) Column = initColumn, ColumnWidth = columnWidth, Order = order, + SourceBullet = listInfo.SourceBullet, BeforeWhitespace = whitespaceBefore, Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), diff --git a/src/Markdig/Parsers/ListInfo.cs b/src/Markdig/Parsers/ListInfo.cs index f7b475dd6..0346d1af7 100644 --- a/src/Markdig/Parsers/ListInfo.cs +++ b/src/Markdig/Parsers/ListInfo.cs @@ -2,6 +2,8 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; + namespace Markdig.Parsers { /// @@ -19,6 +21,7 @@ public ListInfo(char bulletType) OrderedStart = null; OrderedDelimiter = (char)0; DefaultOrderedStart = null; + SourceBullet = StringSlice.Empty; } /// @@ -34,6 +37,7 @@ public ListInfo(char bulletType, string orderedStart, char orderedDelimiter, str OrderedStart = orderedStart; OrderedDelimiter = orderedDelimiter; DefaultOrderedStart = defaultOrderedStart; + SourceBullet = StringSlice.Empty; } /// @@ -55,5 +59,7 @@ public ListInfo(char bulletType, string orderedStart, char orderedDelimiter, str /// Gets or sets default string used as a starting sequence for the ordered list (e.g: '1' for an numbered ordered list) /// public string DefaultOrderedStart { get; set; } + + public StringSlice SourceBullet { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/NumberedListItemParser.cs b/src/Markdig/Parsers/NumberedListItemParser.cs index 2650bd2fb..c418b5e6f 100644 --- a/src/Markdig/Parsers/NumberedListItemParser.cs +++ b/src/Markdig/Parsers/NumberedListItemParser.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Helpers; +using System.Text; namespace Markdig.Parsers { @@ -28,6 +29,7 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out { result = new ListInfo(); var c = state.CurrentChar; + var sourcePosition = state.Start; int countDigit = 0; int startChar = -1; @@ -44,6 +46,7 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out c = state.NextChar(); countDigit++; } + var sourceBullet = new StringSlice(state.Line.Text, sourcePosition, state.Start - 1); if (startChar < 0) { startChar = endChar; @@ -59,6 +62,7 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out result.OrderedDelimiter = orderedDelimiter; result.BulletType = '1'; result.DefaultOrderedStart = "1"; + result.SourceBullet = sourceBullet; return true; } } diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 61fa6c155..2ef58330d 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -43,8 +43,8 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } renderer.Write(listItem.BeforeWhitespace); - renderer.Write(index.ToString(CultureInfo.InvariantCulture)); - //renderer.Write(listItem.Order.ToString(CultureInfo.InvariantCulture)); + //renderer.Write(index.ToString(CultureInfo.InvariantCulture)); + renderer.Write(listItem.SourceBullet); renderer.Write(listBlock.OrderedDelimiter); renderer.Write(' '); renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); diff --git a/src/Markdig/Syntax/ListItemBlock.cs b/src/Markdig/Syntax/ListItemBlock.cs index 412cfa21f..3ecd520c6 100644 --- a/src/Markdig/Syntax/ListItemBlock.cs +++ b/src/Markdig/Syntax/ListItemBlock.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; namespace Markdig.Syntax @@ -26,5 +27,7 @@ public ListItemBlock(BlockParser parser) : base(parser) /// The number defined for this in an ordered list /// public int Order { get; set; } + + public StringSlice SourceBullet { get; set; } } } \ No newline at end of file From e6b591c0351b2f61b8114bae8f7c880249bb3b30 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 15:32:14 +0200 Subject: [PATCH 071/120] fix escapes in FencedCodeBlock info and arguments --- src/Markdig/Extensions/CustomContainers/CustomContainer.cs | 2 ++ src/Markdig/Parsers/FencedBlockParserBase.cs | 2 ++ src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs | 5 ++--- src/Markdig/Syntax/FencedCodeBlock.cs | 2 ++ src/Markdig/Syntax/IFencedBlock.cs | 2 ++ 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index fb67248e3..ca456922c 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -28,10 +28,12 @@ public CustomContainer(BlockParser parser) : base(parser) public string WhitespaceAfterFencedChar { get; set; } public string Info { get; set; } + public string UnescapedInfo { get; set; } public string WhitespaceAfterInfo { get; set; } public string Arguments { get; set; } + public string UnescapedArguments { get; set; } public string WhitespaceAfterArguments { get; set; } public int OpeningFencedCharCount { get; set; } diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 986b5002a..d25b842a5 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -162,8 +162,10 @@ public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice end: fenced.WhitespaceAfterFencedChar = afterFence; fenced.Info = HtmlHelper.Unescape(info); + fenced.UnescapedInfo = info; fenced.WhitespaceAfterInfo = afterInfo; fenced.Arguments = HtmlHelper.Unescape(arg); + fenced.UnescapedArguments = arg; fenced.WhitespaceAfterArguments = afterArg; fenced.InfoNewline = line.Newline; diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index e7881a86e..4130fe54b 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -31,7 +31,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) } if (fencedCodeBlock.Info != null) { - renderer.Write(fencedCodeBlock.Info); + renderer.Write(fencedCodeBlock.UnescapedInfo); } if (fencedCodeBlock.WhitespaceAfterInfo != null) { @@ -39,8 +39,7 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) } if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) { - renderer - .Write(fencedCodeBlock.Arguments); + renderer.Write(fencedCodeBlock.UnescapedArguments); } if (fencedCodeBlock.WhitespaceAfterArguments != null) { diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index 682f1c459..908f8e068 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -30,6 +30,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// public string Info { get; set; } + public string UnescapedInfo { get; set; } /// public string WhitespaceAfterInfo { get; set; } @@ -39,6 +40,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) /// May be null. /// public string Arguments { get; set; } + public string UnescapedArguments { get; set; } /// public string WhitespaceAfterArguments { get; set; } diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 9dd3b167e..0245fd3bd 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -16,6 +16,7 @@ public interface IFencedBlock : IBlock /// the fenced code block. May be null. /// string Info { get; set; } + public string UnescapedInfo { get; set; } /// /// Gets or sets the language parsed after the first line of @@ -28,6 +29,7 @@ public interface IFencedBlock : IBlock /// May be null. /// string Arguments { get; set; } + public string UnescapedArguments { get; set; } /// /// Gets or sets the arguments after the . From bb1da73da2e96c5abf39b1540180850b64c750dd Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 15:53:34 +0200 Subject: [PATCH 072/120] fix whitespace in HtmlBlock within ListItemBlock --- src/Markdig/Parsers/BlockProcessor.cs | 9 ++++++--- src/Markdig/Parsers/HtmlBlockParser.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index b2567540c..09b57f733 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -876,10 +876,13 @@ private void ProcessNewBlocks(BlockState result, bool allowClosing) { if (!result.IsDiscard()) { - bool isParagraph = block is ParagraphBlock; - if (TrackTrivia && isParagraph) + if (TrackTrivia) { - UnwindAllIndents(); + if (block is ParagraphBlock || + block is HtmlBlock) + { + UnwindAllIndents(); + } } leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } diff --git a/src/Markdig/Parsers/HtmlBlockParser.cs b/src/Markdig/Parsers/HtmlBlockParser.cs index 52cb7841b..c515eb515 100644 --- a/src/Markdig/Parsers/HtmlBlockParser.cs +++ b/src/Markdig/Parsers/HtmlBlockParser.cs @@ -277,7 +277,7 @@ private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int Type = type, // By default, setup to the end of line Span = new SourceSpan(startPosition, startPosition + state.Line.End), - BeforeWhitespace = state.PopBeforeWhitespace(startPosition - 1), + //BeforeWhitespace = state.PopBeforeWhitespace(startPosition - 1), LinesBefore = state.UseLinesBefore(), Newline = state.Line.Newline, }); From 694a764f963b09da3b1b30cc98b9057ab355dbfb Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 24 Oct 2020 16:18:33 +0200 Subject: [PATCH 073/120] fix escaped within local label of InlineLink --- src/Markdig/Parsers/Inlines/LinkInlineParser.cs | 9 +++++---- .../Renderers/Normalize/Inlines/LinkInlineRenderer.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 55ee01e52..1fc7e3f92 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -112,6 +112,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) private bool ProcessLinkReference( InlineProcessor state, string label, + string labelWithWhitespace, bool isShortcut, SourceSpan labelSpan, LinkDelimiterInline parent, @@ -140,7 +141,7 @@ private bool ProcessLinkReference( Title = HtmlHelper.Unescape(linkRef.Title), Label = label, LabelSpan = labelSpan, - LabelWithWhitespace = linkRef.LabelWithWhitespace, // TODO + LabelWithWhitespace = labelWithWhitespace, LinkRefDefLabel = linkRef.Label, LinkRefDefLabelWithWhitespace = linkRef.LabelWithWhitespace, LocalLabel = localLabel, @@ -332,6 +333,7 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice var labelSpan = SourceSpan.Empty; string label = null; + string labelWithWhitespace = null; bool isLabelSpanLocal = true; bool isShortcut = false; @@ -355,15 +357,14 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice label = openParent.Label; isShortcut = true; } - - if (label != null || LinkHelper.TryParseLabel(ref text, true, out label, out labelSpan)) + if (label != null || LinkHelper.TryParseLabelWhitespace(ref text, true, out label, out labelWithWhitespace, out labelSpan)) { if (isLabelSpanLocal) { labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); } - if (ProcessLinkReference(inlineState, label, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) + if (ProcessLinkReference(inlineState, label, labelWithWhitespace, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) { // Remove the open parent openParent.Remove(); diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index 167cc20bd..ac03cbcea 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -30,7 +30,7 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) renderer.Write('['); if (link.LocalLabel == LocalLabel.Local) { - renderer.Write(link.Label); + renderer.Write(link.LabelWithWhitespace); } renderer.Write(']'); } From 24be820827db77046c6e0c486a54085a85e0548e Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 11:32:25 +0100 Subject: [PATCH 074/120] omit LinkReferenceDefinitionGroup and insert LinkReferenceDefinitions at proper indexes of document/parent --- .../AutoIdentifiers/AutoIdentifierExtension.cs | 2 +- src/Markdig/Extensions/Footnotes/FootnoteParser.cs | 2 +- src/Markdig/Parsers/ParagraphBlockParser.cs | 7 +++++-- .../Syntax/LinkReferenceDefinitionExtensions.cs | 12 ++++++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs index 4f4ad1001..25a706d29 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs @@ -107,7 +107,7 @@ private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline inl // If it is the case, we skip the auto identifier for the Heading if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef)) { - doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value); + doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value, true); } } // Once we are done, we don't need to keep the intermediate dictionary around diff --git a/src/Markdig/Extensions/Footnotes/FootnoteParser.cs b/src/Markdig/Extensions/Footnotes/FootnoteParser.cs index e77b79eef..d70cc8253 100644 --- a/src/Markdig/Extensions/Footnotes/FootnoteParser.cs +++ b/src/Markdig/Extensions/Footnotes/FootnoteParser.cs @@ -79,7 +79,7 @@ private BlockState TryOpen(BlockProcessor processor, bool isContinue) LabelSpan = labelSpan, Label = label }; - processor.Document.SetLinkReferenceDefinition(footnote.Label, linkRef); + processor.Document.SetLinkReferenceDefinition(footnote.Label, linkRef, true); processor.NewBlocks.Push(footnote); return BlockState.Continue; } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 293146443..99ebd33ff 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -173,7 +173,7 @@ private static bool TryMatchLinkReferenceDefinition(ref StringLineGroup lines, B var iterator = lines.ToCharIterator(); if (LinkReferenceDefinition.TryParse(ref iterator, out LinkReferenceDefinition linkReferenceDefinition)) { - state.Document.SetLinkReferenceDefinition(linkReferenceDefinition.Label, linkReferenceDefinition); + state.Document.SetLinkReferenceDefinition(linkReferenceDefinition.Label, linkReferenceDefinition, true); atLeastOneFound = true; // Correct the locations of each field @@ -212,7 +212,8 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou out SourceSpan whitespaceBeforeTitle, out SourceSpan whitespaceAfterTitle)) { - state.Document.SetLinkReferenceDefinition(lrd.Label, lrd); + state.Document.SetLinkReferenceDefinition(lrd.Label, lrd, false); + lrd.Parent = null; // remove LRDG parent from lrd atLeastOneFound = true; // Correct the locations of each field @@ -237,6 +238,8 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou state.BeforeLines = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack lines = iterator.Remaining(); + var index = paragraph.Parent.IndexOf(paragraph); + paragraph.Parent.Insert(index, lrd); } else { diff --git a/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs b/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs index 0bc6c2331..299aca9e1 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinitionExtensions.cs @@ -24,10 +24,10 @@ public static bool ContainsLinkReferenceDefinition(this MarkdownDocument documen return references.Links.ContainsKey(label); } - public static void SetLinkReferenceDefinition(this MarkdownDocument document, string label, LinkReferenceDefinition linkReferenceDefinition) + public static void SetLinkReferenceDefinition(this MarkdownDocument document, string label, LinkReferenceDefinition linkReferenceDefinition, bool addGroup) { if (label == null) ThrowHelper.ArgumentNullException_label(); - var references = document.GetLinkReferenceDefinitions(); + var references = document.GetLinkReferenceDefinitions(addGroup); references.Set(label, linkReferenceDefinition); } @@ -43,14 +43,18 @@ public static bool TryGetLinkReferenceDefinition(this MarkdownDocument document, return references.TryGet(label, out linkReferenceDefinition); } - public static LinkReferenceDefinitionGroup GetLinkReferenceDefinitions(this MarkdownDocument document) + public static LinkReferenceDefinitionGroup GetLinkReferenceDefinitions(this MarkdownDocument document, bool addGroup) { var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup; if (references == null) { references = new LinkReferenceDefinitionGroup(); document.SetData(DocumentKey, references); - document.Add(references); + // don't add the LinkReferenceDefinitionGroup when tracking trivia + if (addGroup) + { + document.Add(references); + } } return references; } From 420aa79a48db498095ff9dd495a9e56e62019b1e Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 13:15:46 +0100 Subject: [PATCH 075/120] fix LinkReferenceDefinition bugs --- .../RoundtripSpecs/TestLinkReferenceDefinition.cs | 2 ++ src/Markdig/Helpers/LinkHelper.cs | 4 ++++ src/Markdig/Parsers/ParagraphBlockParser.cs | 12 +++++++++++- .../Normalize/LinkReferenceDefinitionRenderer.cs | 2 +- src/Markdig/Renderers/Normalize/NormalizeRenderer.cs | 1 + src/Markdig/Renderers/TextRendererBase.cs | 2 +- 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index 8133af06e..4d41134f1 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -155,6 +155,8 @@ public void TestUncommonWhitespace(string value) [TestCase("[a]:\n/r\n\"t\r\nt\"")] [TestCase("[a]:\r\n /r\t \n \t \"t\r\nt\" ")] + [TestCase("[a]:\n/r\n\n[a],")] + [TestCase("[a]: /r\n[b]: /r\n\n[a],")] public void TestNewlines(string value) { RoundTrip(value); diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index e937391cb..de7ba5def 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -1279,12 +1279,16 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( { text = saved; title = null; + unescapedTitle = null; + whitespaceAfterTitle = SourceSpan.Empty; return true; } label = null; url = null; + unescapedUrl = null; title = null; + unescapedTitle = null; return false; } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 99ebd33ff..f510fbe8d 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -95,9 +95,19 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) { var paragraph = (ParagraphBlock)block; + bool foundLrd; + if (state.TrackTrivia) + { + foundLrd = TryMatchLinkReferenceDefinitionWhitespace(ref paragraph.Lines, state, paragraph); + } + else + { + foundLrd = TryMatchLinkReferenceDefinition(ref paragraph.Lines, state); + } + // If we matched a LinkReferenceDefinition before matching the heading, and the remaining // lines are empty, we can early exit and remove the paragraph - if (!(TryMatchLinkReferenceDefinition(ref paragraph.Lines, state) && paragraph.Lines.Count == 0)) + if (!foundLrd && paragraph.Lines.Count == 0) { // We discard the paragraph that will be transformed to a heading state.Discard(paragraph); diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs index 132f52efe..dbcf867f5 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs @@ -29,7 +29,7 @@ protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitio } renderer.Write(linkDef.WhitespaceBeforeTitle); - if (linkDef.Title != null) + if (linkDef.UnescapedTitle != null) { var open = linkDef.TitleEnclosingCharacter; var close = linkDef.TitleEnclosingCharacter; diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index 8e4e455c5..83f9ff3ac 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -148,6 +148,7 @@ public void RenderLinesBefore(Block block) public void RenderLinesAfter(Block block) { + previousWasLine = true; if (block.LinesAfter == null) { return; diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index a1f6ac2c4..6196d46b3 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -97,7 +97,7 @@ internal string Next() } } - private bool previousWasLine; + protected bool previousWasLine; #if !NETCORE private char[] buffer; #endif From 83357dc92955d1f57253e5288a26ed682de2f977 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 13:33:06 +0100 Subject: [PATCH 076/120] fix whitespace in nested QuoteBlock --- src/Markdig/Parsers/QuoteBlockParser.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 8bb2066c3..8cbadd196 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -86,7 +86,6 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) quote.QuoteLines.Add(new QuoteBlock.QuoteLine { QuoteChar = false, - BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), Newline = processor.Line.Newline, }); return BlockState.None; From 813647ca10253ce698ac7b206a13ab0673090353 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 14:16:53 +0100 Subject: [PATCH 077/120] place newline on LinkReferenceDefinition instead of leaf whitespace properties --- .../TestLinkReferenceDefinition.cs | 7 ++++++ src/Markdig/Helpers/LinkHelper.cs | 25 ++++++++++++++++--- .../LinkReferenceDefinitionRenderer.cs | 1 + src/Markdig/Syntax/CharIteratorHelper.cs | 15 ++++++++++- src/Markdig/Syntax/LinkReferenceDefinition.cs | 4 ++- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index 4d41134f1..1d7a10173 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -131,6 +131,13 @@ public void Test(string value) RoundTrip(value); } + [TestCase("[a]: /r\n[b]: /r\n")] + [TestCase("[a]: /r\n[b]: /r\n[c] /r\n")] + public void TestMultiple(string value) + { + RoundTrip(value); + } + [TestCase("[a]:\f/r\f\"l\"")] [TestCase("[a]:\v/r\v\"l\"")] public void TestUncommonWhitespace(string value) diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index de7ba5def..53cdb0a17 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -1119,7 +1119,7 @@ public static bool TryParseLinkReferenceDefinition(ref T text, out string lab urlSpan.End = text.Start - 1; var saved = text; - var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount); + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out _); var c = text.CurrentChar; if (c == '\'' || c == '"' || c == '(') { @@ -1186,6 +1186,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( out string title, // can contain non-consecutive newlines out string unescapedTitle, out char titleEnclosingCharacter, + out Newline newline, out SourceSpan whitespaceAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, @@ -1197,6 +1198,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( whitespaceBeforeTitle = SourceSpan.Empty; title = null; unescapedTitle = null; + newline = Newline.None; urlSpan = SourceSpan.Empty; titleSpan = SourceSpan.Empty; @@ -1234,7 +1236,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( int whitespaceBeforeTitleStart = text.Start; var saved = text; - var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount); + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out newline); whitespaceBeforeTitle = new SourceSpan(whitespaceBeforeTitleStart, text.Start - 1); var c = text.CurrentChar; @@ -1249,7 +1251,6 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( { return false; } - whitespaceAfterTitle = new SourceSpan(text.Start, text.End); } else { @@ -1260,12 +1261,14 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( { if (text.CurrentChar == '\0' || newLineCount > 0) { + whitespaceAfterTitle = new SourceSpan(text.Start, text.Start - 1); return true; } } // Check that the current line has only trailing spaces c = text.CurrentChar; + int whitespaceAfterTitleStart = text.Start; while (c.IsSpaceOrTab()) { c = text.NextChar(); @@ -1291,6 +1294,22 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( unescapedTitle = null; return false; } + whitespaceAfterTitle = new SourceSpan(whitespaceAfterTitleStart, text.Start - 1); + if (c != '\0') + { + if (c == '\n') + { + newline = Newline.LineFeed; + } + else if (c == '\r' && text.PeekChar() == '\n') + { + newline = Newline.CarriageReturnLineFeed; + } + else if (c == '\r') + { + newline = Newline.CarriageReturn; + } + } return true; } diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs index dbcf867f5..2871ec9df 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionRenderer.cs @@ -42,6 +42,7 @@ protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinitio renderer.Write(close); } renderer.Write(linkDef.AfterWhitespace); + renderer.Write(linkDef.Newline); renderer.RenderLinesAfter(linkDef); } diff --git a/src/Markdig/Syntax/CharIteratorHelper.cs b/src/Markdig/Syntax/CharIteratorHelper.cs index 4e8939c7a..563a6b8fd 100644 --- a/src/Markdig/Syntax/CharIteratorHelper.cs +++ b/src/Markdig/Syntax/CharIteratorHelper.cs @@ -11,16 +11,29 @@ namespace Markdig.Syntax /// public static class CharIteratorHelper { - public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines) where T : ICharIterator + public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines, out Newline firstNewline) where T : ICharIterator { countNewLines = 0; var c = iterator.CurrentChar; bool hasWhitespaces = false; + firstNewline = Newline.None; while (c.IsWhitespace()) { // TODO: RTP: fix newline check here for \r\n if (c == '\n' || c == '\r') { + if (c == '\r' && iterator.PeekChar() == '\n' && firstNewline != Newline.None) + { + firstNewline = Newline.CarriageReturnLineFeed; + } + else if (c == '\n' && firstNewline != Newline.None) + { + firstNewline = Newline.LineFeed; + } + else if (c == '\r' && firstNewline != Newline.None) + { + firstNewline = Newline.CarriageReturn; + } countNewLines++; } hasWhitespaces = true; diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 8c4ddb8e8..df48c0e42 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -159,6 +159,7 @@ public static bool TryParseWhitespace( out string title, out string unescapedTitle, out char titleEnclosingCharacter, + out Newline newline, out whitespaceAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, @@ -177,7 +178,8 @@ public static bool TryParseWhitespace( UnescapedUrl = unescapedUrl, UnescapedTitle = unescapedTitle, TitleSpan = titleSpan, - Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End) + Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End), + Newline = newline, }; return true; } From c2cfb05d8d21a43c182236af4f0b584430ba4de6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 14:31:25 +0100 Subject: [PATCH 078/120] fix whitespace between heading and headingchar for atx headings --- src/Markdig/Parsers/HeadingBlockParser.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index fc91b3903..bfaa46d90 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -76,6 +76,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (c.IsSpaceOrTab()) { whitespace = new StringSlice(processor.Line.Text, processor.Start + leadingCount, processor.Start + leadingCount); + line.NextChar(); } // Move to the content var headingBlock = new HeadingBlock(this) @@ -84,7 +85,7 @@ public override BlockState TryOpen(BlockProcessor processor) WhitespaceAfterAtxHeaderChar = whitespace, Level = leadingCount, Column = column, - Span = { Start = sourcePosition }, + Span = { Start = line.Start }, BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), Newline = processor.Line.Newline, @@ -135,6 +136,8 @@ public override BlockState TryOpen(BlockProcessor processor) // Setup the source end position of this element headingBlock.Span.End = processor.Line.End; + // feed the line to the BlockProcessor without whitespace + processor.Line.Start = headingBlock.Span.Start; var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); headingBlock.AfterWhitespace = wsa; From e4f2892a239204d9e52d7784a7b8fc80f1bb09d2 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 14:52:23 +0100 Subject: [PATCH 079/120] do "TODO: RTP:" todos --- .../Inlines/TestAutoLinkInline.cs | 1 + src/Markdig/Helpers/CharHelper.cs | 2 +- src/Markdig/Helpers/LinkHelper.cs | 2 +- src/Markdig/Parsers/BlockProcessor.cs | 1 - .../Parsers/Inlines/LinkInlineParser.cs | 6 +++--- src/Markdig/Parsers/ParagraphBlockParser.cs | 5 ++++- .../Renderers/Normalize/CodeBlockRenderer.cs | 2 -- .../Normalize/Inlines/LinkInlineRenderer.cs | 1 - .../Renderers/Normalize/QuoteBlockRenderer.cs | 1 - src/Markdig/Syntax/CharIteratorHelper.cs | 18 +++++++++++++++++- 10 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs index 02d34926f..b5b67b798 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestAutoLinkInline.cs @@ -14,6 +14,7 @@ public class TestAutoLinkInline [TestCase(" ")] [TestCase(" ")] [TestCase(" ")] + [TestCase("p http://a p")] public void Test(string value) { RoundTrip(value); diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index edac1a31d..3a146660b 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -225,7 +225,7 @@ internal static bool IsSpaceOrPunctuation(this char c) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNewLine(this char c) { - return c == '\n'; // TODO: RTP: check + return c == '\n' || c == '\r'; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 53cdb0a17..13b94e8ea 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -1119,7 +1119,7 @@ public static bool TryParseLinkReferenceDefinition(ref T text, out string lab urlSpan.End = text.Start - 1; var saved = text; - var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out _); + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount); var c = text.CurrentChar; if (c == '\'' || c == '"' || c == '(') { diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 09b57f733..b59081f51 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -812,7 +812,6 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (!result.IsDiscard()) { - // TODO: RTP: pass line with whitespace if (TrackTrivia) { UnwindAllIndents(); diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 1fc7e3f92..32d0acb54 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -252,11 +252,11 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice var link = new LinkInline() { WhitespaceBeforeUrl = wsBeforeLink, - Url = HtmlHelper.Unescape(url), // TODO: RTP: unescape - UnescapedUrl = unescapedUrl, // TODO: RTP: unescape + Url = HtmlHelper.Unescape(url), + UnescapedUrl = unescapedUrl, UrlHasPointyBrackets = urlHasPointyBrackets, WhitespaceAfterUrl = wsAfterLink, - Title = HtmlHelper.Unescape(title), // TODO: RTP: unescape + Title = HtmlHelper.Unescape(title), UnescapedTitle = unescapedTitle, TitleEnclosingCharacter = titleEnclosingCharacter, WhitespaceAfterTitle = wsAfterTitle, diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index f510fbe8d..f8f96ccc1 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -133,7 +133,10 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) HeaderCharCount = count, SetextNewline = paragraph.Newline, }; - //heading.Lines.Trim(); // TODO: RTP: fix + if (!state.TrackTrivia) + { + heading.Lines.Trim(); + } // Remove the paragraph as a pending block state.NewBlocks.Push(heading); diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index 4130fe54b..c83de9201 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -66,8 +66,6 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) // See example 207: "> ```\nfoo\n```" renderer.WriteLine(obj.Newline); } - // TODO: RTP: the spec is unclear about whitespace after closing fence - // imlementations seem to accept it though renderer.Write(obj.AfterWhitespace); } else diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index ac03cbcea..a08a58c7b 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -61,7 +61,6 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) close = ')'; } renderer.Write(open); - //renderer.Write(link.Title.Replace(@"""", @"\""")); // TODO: RTP: should this always be done? renderer.Write(link.UnescapedTitle); renderer.Write(close); renderer.Write(link.WhitespaceAfterTitle); diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index 373d2373e..d78aae9ba 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -36,7 +36,6 @@ protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) // Wanted: a more elegant/better solution (although this is not *that* bad). foreach (var quoteLine in quoteBlock.QuoteLines) { - // TODO: RTP: introduce EmptyQuoteLine class deriving from LeafBlock? var emptyLeafBlock = new ParagraphBlock { Newline = quoteLine.Newline diff --git a/src/Markdig/Syntax/CharIteratorHelper.cs b/src/Markdig/Syntax/CharIteratorHelper.cs index 563a6b8fd..bc046e9d7 100644 --- a/src/Markdig/Syntax/CharIteratorHelper.cs +++ b/src/Markdig/Syntax/CharIteratorHelper.cs @@ -11,6 +11,23 @@ namespace Markdig.Syntax /// public static class CharIteratorHelper { + public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines) where T : ICharIterator + { + countNewLines = 0; + var c = iterator.CurrentChar; + bool hasWhitespaces = false; + while (c.IsWhitespace()) + { + if (c == '\n') + { + countNewLines++; + } + hasWhitespaces = true; + c = iterator.NextChar(); + } + return hasWhitespaces; + } + public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines, out Newline firstNewline) where T : ICharIterator { countNewLines = 0; @@ -19,7 +36,6 @@ public static bool TrimStartAndCountNewLines(ref T iterator, out int countNew firstNewline = Newline.None; while (c.IsWhitespace()) { - // TODO: RTP: fix newline check here for \r\n if (c == '\n' || c == '\r') { if (c == '\r' && iterator.PeekChar() == '\n' && firstNewline != Newline.None) From 068a6e7af593cca70cfa39bdda9205553bb5ea9a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 14:54:52 +0100 Subject: [PATCH 080/120] check IsNewline calls --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 4 ++-- src/Markdig/Helpers/CharHelper.cs | 6 ++++++ src/Markdig/Helpers/LinkHelper.cs | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 49828ff31..f79829b8a 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -34,8 +34,8 @@ In order: - ~~support AutolinkInline~~ - ~~generate spec examples as tests for roundtrip~~ - fix `TODO: RTP: ` -- check char.IsWhitespace() calls -- check char.IsNewline() calls +- ~~check char.IsWhitespace() calls~~ +- ~~check char.IsNewline() calls~~ - introduce feature flag - extract MarkdownRenderer - cleanup NormalizeRenderer (MarkdownRenderer) diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index 3a146660b..fcc82138c 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -222,6 +222,12 @@ internal static bool IsSpaceOrPunctuation(this char c) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNewLineLineFeed(this char c) + { + return c == '\n' || c == '\r'; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNewLine(this char c) { diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 13b94e8ea..7b6ec2254 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -762,7 +762,7 @@ public static bool TryParseUrl(ref T text, out string link, out bool hasPoint continue; } - if (c.IsNewLine()) + if (c.IsNewLineLineFeed()) { break; } From 4814e9cea5e72c05a90654c7355aafee3652b431 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 15:21:13 +0100 Subject: [PATCH 081/120] introduce trackTrivia feature flag --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 2 +- src/Markdig.Tests/TestRoundtrip.cs | 2 +- src/Markdig/Markdown.cs | 8 +-- src/Markdig/Parsers/BlockProcessor.cs | 58 +++++++++---------- src/Markdig/Parsers/InlineProcessor.cs | 12 ++-- src/Markdig/Parsers/MarkdownParser.cs | 13 +++-- .../Syntax/Inlines/EscapedCharacterInline.cs | 12 ---- 7 files changed, 47 insertions(+), 60 deletions(-) delete mode 100644 src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index f79829b8a..960bc3351 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -36,7 +36,7 @@ In order: - fix `TODO: RTP: ` - ~~check char.IsWhitespace() calls~~ - ~~check char.IsNewline() calls~~ -- introduce feature flag +- ~~introduce feature flag~~ - extract MarkdownRenderer - cleanup NormalizeRenderer (MarkdownRenderer) - deduplicate MarkdownRenderer and NormalizeRenderer code diff --git a/src/Markdig.Tests/TestRoundtrip.cs b/src/Markdig.Tests/TestRoundtrip.cs index 1e6a57f06..c56f623ae 100644 --- a/src/Markdig.Tests/TestRoundtrip.cs +++ b/src/Markdig.Tests/TestRoundtrip.cs @@ -16,7 +16,7 @@ internal static void RoundTrip(string markdown) { var pipelineBuilder = new MarkdownPipelineBuilder(); MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline, trackTrivia: true); var sw = new StringWriter(); var nr = new NormalizeRenderer(sw); diff --git a/src/Markdig/Markdown.cs b/src/Markdig/Markdown.cs index 813cbbabb..cc4af7c4a 100644 --- a/src/Markdig/Markdown.cs +++ b/src/Markdig/Markdown.cs @@ -138,10 +138,10 @@ public static object Convert(string markdown, IMarkdownRenderer renderer, Markdo /// The markdown text. /// An AST Markdown document /// if markdown variable is null - public static MarkdownDocument Parse(string markdown) + public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) { if (markdown == null) ThrowHelper.ArgumentNullException_markdown(); - return Parse(markdown, null); + return Parse(markdown, null, trackTrivia: trackTrivia); } /// @@ -152,13 +152,13 @@ public static MarkdownDocument Parse(string markdown) /// A parser context used for the parsing. /// An AST Markdown document /// if markdown variable is null - public static MarkdownDocument Parse(string markdown, MarkdownPipeline pipeline, MarkdownParserContext context = null) + public static MarkdownDocument Parse(string markdown, MarkdownPipeline pipeline, MarkdownParserContext context = null, bool trackTrivia = false) { if (markdown == null) ThrowHelper.ArgumentNullException_markdown(); pipeline ??= new MarkdownPipelineBuilder().Build(); pipeline = CheckForSelfPipeline(pipeline, markdown); - return MarkdownParser.Parse(markdown, pipeline, context); + return MarkdownParser.Parse(markdown, pipeline, context, trackTrivia); } private static MarkdownPipeline CheckForSelfPipeline(MarkdownPipeline pipeline, string markdown) diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index b59081f51..ff5ade428 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -20,13 +20,13 @@ public class BlockProcessor private int currentStackIndex; private readonly BlockParserStateCache parserStateCache; private int originalLineStart = 0; - private bool trackTrivia = true; - private BlockProcessor(BlockProcessor root) + private BlockProcessor(BlockProcessor root, bool trackTrivia = false) { // These properties are not changing between a parent and a children BlockProcessor this.root = root; this.parserStateCache = root.parserStateCache; + TrackTrivia = trackTrivia; Document = root.Document; Parsers = root.Parsers; @@ -50,10 +50,11 @@ internal List UseLinesBefore() /// A parser context used for the parsing. /// /// - public BlockProcessor(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext context) + public BlockProcessor(MarkdownDocument document, BlockParserList parsers, MarkdownParserContext context, bool trackTrivia = false) { if (document == null) ThrowHelper.ArgumentNullException(nameof(document)); if (parsers == null) ThrowHelper.ArgumentNullException(nameof(parsers)); + TrackTrivia = trackTrivia; parserStateCache = new BlockParserStateCache(this); Document = document; document.IsOpen = true; @@ -165,18 +166,7 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo /// private List OpenedBlocks { get; } - /// - /// Gets or sets a value indicating whether to continue processing the current line. - /// - private bool _cpl; - private bool ContinueProcessingLine { get => _cpl; - set - { - - - _cpl = value; - } - } + private bool ContinueProcessingLine { get; set; } public int WhitespaceStart { get; set; } @@ -189,7 +179,8 @@ public StringSlice PopBeforeWhitespace(int end) } public List BeforeLines { get; set; } - public bool TrackTrivia { get => trackTrivia; set => trackTrivia = value; } + + public bool TrackTrivia { get; set; } = true; /// /// Get the current Container that is currently opened @@ -689,10 +680,13 @@ private void TryContinueBlocks() { if (Line.IsEmpty) { - BeforeLines ??= new List(); - var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); - BeforeLines.Add(line); - Line.Start = StartBeforeIndent; + if (TrackTrivia) + { + BeforeLines ??= new List(); + var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); + BeforeLines.Add(line); + Line.Start = StartBeforeIndent; + } } ContinueProcessingLine = false; break; @@ -765,11 +759,14 @@ private bool TryOpenBlocks(BlockParser[] parsers) var blockParser = parsers[j]; if (Line.IsEmpty) { - BeforeLines ??= new List(); - var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); - BeforeLines.Add(line); + if (TrackTrivia) + { + BeforeLines ??= new List(); + var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); + BeforeLines.Add(line); + Line.Start = StartBeforeIndent; + } ContinueProcessingLine = false; - Line.Start = StartBeforeIndent; break; } @@ -818,12 +815,15 @@ private bool TryOpenBlocks(BlockParser[] parsers) } paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } - // TODO: RTP: delegate this to container parser classes - var qb = paragraph.Parent as QuoteBlock; - if (qb != null) + if (TrackTrivia) { - var afterWhitespace = PopBeforeWhitespace(Start - 1); - qb.QuoteLines.Last().AfterWhitespace = afterWhitespace; + // TODO: RTP: delegate this to container parser classes + var qb = paragraph.Parent as QuoteBlock; + if (qb != null) + { + var afterWhitespace = PopBeforeWhitespace(Start - 1); + qb.QuoteLines.Last().AfterWhitespace = afterWhitespace; + } } // We have just found a lazy continuation for a paragraph, early exit // Mark all block opened after a lazy continuation diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index d55886a6d..4955090dd 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -37,18 +37,18 @@ public class InlineProcessor /// A parser context used for the parsing. /// /// - public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext context) + public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation, MarkdownParserContext context, bool trackTrivia = false) { if (document == null) ThrowHelper.ArgumentNullException(nameof(document)); if (parsers == null) ThrowHelper.ArgumentNullException(nameof(parsers)); Document = document; Parsers = parsers; Context = context; + TrackTrivia = trackTrivia; PreciseSourceLocation = preciseSourcelocation; lineOffsets = new List(); ParserStates = new object[Parsers.Count]; LiteralInlineParser = new LiteralInlineParser(); - LineBreakInlineParser = new LineBreakInlineParser(); } /// @@ -113,8 +113,6 @@ public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool /// public LiteralInlineParser LiteralInlineParser { get; } - public LineBreakInlineParser LineBreakInlineParser { get; } - public int GetSourcePosition(int sliceOffset) { @@ -190,8 +188,6 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) lineOffsets.Clear(); var text = leafBlock.Lines.ToSlice(lineOffsets); leafBlock.Lines.Release(); - ContainerInline container = null; - int previousStart = -1; while (!text.IsEmpty) @@ -242,7 +238,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) if (nextInline.Parent == null) { // Get deepest container - container = FindLastContainer(); + var container = FindLastContainer(); if (!ReferenceEquals(container, nextInline)) { container.AppendChild(nextInline); @@ -262,7 +258,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) else { // Get deepest container - container = FindLastContainer(); + var container = FindLastContainer(); Inline = container.LastChild is LeafInline ? container.LastChild : container; if (Inline == Root) diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 53889d620..5714e608c 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -26,6 +26,8 @@ public sealed class MarkdownParser private readonly ProcessDocumentDelegate documentProcessed; private readonly bool preciseSourceLocation; + public bool TrackTrivia { get; } + private readonly int roughLineCountEstimate; private LineReader lineReader; @@ -38,11 +40,12 @@ public sealed class MarkdownParser /// A parser context used for the parsing. /// /// - private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserContext context) + private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserContext context, bool trackTrivia = false) { if (text == null) ThrowHelper.ArgumentNullException_text(); if (pipeline == null) ThrowHelper.ArgumentNullException(nameof(pipeline)); + TrackTrivia = trackTrivia; roughLineCountEstimate = text.Length / 40; text = FixupZero(text); lineReader = new LineReader(text); @@ -52,10 +55,10 @@ private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserCon document = new MarkdownDocument(); // Initialize the block parsers - blockProcessor = new BlockProcessor(document, pipeline.BlockParsers, context); + blockProcessor = new BlockProcessor(document, pipeline.BlockParsers, context, trackTrivia); // Initialize the inline parsers - inlineProcessor = new InlineProcessor(document, pipeline.InlineParsers, pipeline.PreciseSourceLocation, context) + inlineProcessor = new InlineProcessor(document, pipeline.InlineParsers, pipeline.PreciseSourceLocation, context, trackTrivia) { DebugLog = pipeline.DebugLog }; @@ -71,13 +74,13 @@ private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserCon /// A parser context used for the parsing. /// An AST Markdown document /// if reader variable is null - public static MarkdownDocument Parse(string text, MarkdownPipeline pipeline = null, MarkdownParserContext context = null) + public static MarkdownDocument Parse(string text, MarkdownPipeline pipeline = null, MarkdownParserContext context = null, bool trackTrivia = false) { if (text == null) ThrowHelper.ArgumentNullException_text(); pipeline ??= new MarkdownPipelineBuilder().Build(); // Perform the parsing - var markdownParser = new MarkdownParser(text, pipeline, context); + var markdownParser = new MarkdownParser(text, pipeline, context, trackTrivia); return markdownParser.Parse(); } diff --git a/src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs b/src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs deleted file mode 100644 index 1a4d11dfb..000000000 --- a/src/Markdig/Syntax/Inlines/EscapedCharacterInline.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics; - -namespace Markdig.Syntax.Inlines -{ - [DebuggerDisplay("\\{Character}")] - public class EscapedCharacterInline : LeafInline - { - public char Character { get; set; } - - public override string ToString() => $@"\\{Character}"; - } -} From 0e8c312fda7fba5b12ccc817701355346baaeff6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 15:43:27 +0100 Subject: [PATCH 082/120] extract RoundtripRenderer and restore NormalizeRenderer --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 2 +- src/Markdig.Tests/TestFencedCodeBlock.cs | 4 +- src/Markdig.Tests/TestQuoteBlock.cs | 32 ---- src/Markdig.Tests/TestRoundtrip.cs | 4 +- .../Renderers/Normalize/CodeBlockRenderer.cs | 66 +------- .../Renderers/Normalize/HeadingRenderer.cs | 37 +--- .../Renderers/Normalize/HtmlBlockRenderer.cs | 5 +- .../Normalize/Inlines/CodeInlineRenderer.cs | 32 +++- .../Inlines/LineBreakInlineRenderer.cs | 7 +- .../Normalize/Inlines/LinkInlineRenderer.cs | 45 ++--- .../LinkReferenceDefinitionGroupRenderer.cs | 2 + .../LinkReferenceDefinitionRenderer.cs | 39 +---- .../Renderers/Normalize/ListRenderer.cs | 45 ++--- .../Renderers/Normalize/NormalizeRenderer.cs | 59 ++++--- .../Renderers/Normalize/ParagraphRenderer.cs | 11 +- .../Renderers/Normalize/QuoteBlockRenderer.cs | 49 +----- .../Normalize/SetextHeadingRenderer.cs | 10 -- .../Normalize/ThematicBreakRenderer.cs | 11 +- .../Renderers/Roundtrip/CodeBlockRenderer.cs | 102 +++++++++++ .../Renderers/Roundtrip/HeadingRenderer.cs | 61 +++++++ .../Renderers/Roundtrip/HtmlBlockRenderer.cs | 19 +++ .../Inlines/AutolinkInlineRenderer.cs | 20 +++ .../Roundtrip/Inlines/CodeInlineRenderer.cs | 26 +++ .../Inlines/DelimiterInlineRenderer.cs | 21 +++ .../Inlines/EmphasisInlineRenderer.cs | 23 +++ .../Inlines/LineBreakInlineRenderer.cs | 30 ++++ .../Roundtrip/Inlines/LinkInlineRenderer.cs | 74 ++++++++ .../Inlines/LiteralInlineRenderer.cs | 25 +++ .../RoundtripHtmlEntityInlineRenderer.cs | 19 +++ .../Inlines/RoundtripHtmlInlineRenderer.cs | 19 +++ .../LinkReferenceDefinitionGroupRenderer.cs | 16 ++ .../LinkReferenceDefinitionRenderer.cs | 50 ++++++ .../Renderers/Roundtrip/ListRenderer.cs | 110 ++++++++++++ .../Renderers/Roundtrip/ParagraphRenderer.cs | 26 +++ .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 64 +++++++ .../Roundtrip/RoundtripObjectRenderer.cs | 17 ++ .../Renderers/Roundtrip/RoundtripRenderer.cs | 159 ++++++++++++++++++ .../Roundtrip/ThematicBreakRenderer.cs | 28 +++ 38 files changed, 1042 insertions(+), 327 deletions(-) delete mode 100644 src/Markdig.Tests/TestQuoteBlock.cs delete mode 100644 src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/ListRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs create mode 100644 src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 960bc3351..8305b0eb5 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -37,7 +37,7 @@ In order: - ~~check char.IsWhitespace() calls~~ - ~~check char.IsNewline() calls~~ - ~~introduce feature flag~~ -- extract MarkdownRenderer +- ~~extract MarkdownRenderer~~ - cleanup NormalizeRenderer (MarkdownRenderer) - deduplicate MarkdownRenderer and NormalizeRenderer code - do pull request feedback diff --git a/src/Markdig.Tests/TestFencedCodeBlock.cs b/src/Markdig.Tests/TestFencedCodeBlock.cs index e18d1c4f7..180339125 100644 --- a/src/Markdig.Tests/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/TestFencedCodeBlock.cs @@ -1,10 +1,12 @@ using Markdig.Renderers.Normalize; +using Markdig.Renderers.Roundtrip; using Markdig.Syntax; using NUnit.Framework; using System.IO; namespace Markdig.Tests { + // TODO: RTP: integrate with roundtrip synthetic tests [TestFixture] public partial class TestFencedCodeBlock { @@ -58,7 +60,7 @@ public void CstInfoParser_RoundTrip() MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); + var nr = new RoundtripRenderer(sw); nr.Write(markdownDocument); Assert.AreEqual(markdown, sw.ToString()); diff --git a/src/Markdig.Tests/TestQuoteBlock.cs b/src/Markdig.Tests/TestQuoteBlock.cs deleted file mode 100644 index 7654a6a36..000000000 --- a/src/Markdig.Tests/TestQuoteBlock.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Markdig.Renderers.Normalize; -using Markdig.Syntax; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Markdig.Tests -{ - [TestFixture] - public class TestQuoteBlock - { - [Test] - public void Test() - { - var markdown = " > This is a quote with whitespace before, between and after. "; - - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var paragraphBlock = markdownDocument[0] as QuoteBlock; - - var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - } -} diff --git a/src/Markdig.Tests/TestRoundtrip.cs b/src/Markdig.Tests/TestRoundtrip.cs index c56f623ae..2f491530d 100644 --- a/src/Markdig.Tests/TestRoundtrip.cs +++ b/src/Markdig.Tests/TestRoundtrip.cs @@ -1,4 +1,4 @@ -using Markdig.Renderers.Normalize; +using Markdig.Renderers.Roundtrip; using Markdig.Syntax; using NUnit.Framework; using System.IO; @@ -18,7 +18,7 @@ internal static void RoundTrip(string markdown) MarkdownPipeline pipeline = pipelineBuilder.Build(); MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline, trackTrivia: true); var sw = new StringWriter(); - var nr = new NormalizeRenderer(sw); + var nr = new RoundtripRenderer(sw); nr.Write(markdownDocument); diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index c83de9201..c9cb26136 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -2,9 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using Markdig.Helpers; using Markdig.Syntax; -using System.Collections.Generic; namespace Markdig.Renderers.Normalize { @@ -18,32 +16,17 @@ public class CodeBlockRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, CodeBlock obj) { - renderer.RenderLinesBefore(obj); if (obj is FencedCodeBlock fencedCodeBlock) { - renderer.Write(obj.BeforeWhitespace); - var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); + var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.FencedCharCount); renderer.Write(opening); - - if (fencedCodeBlock.WhitespaceAfterFencedChar != null) - { - renderer.Write(fencedCodeBlock.WhitespaceAfterFencedChar); - } if (fencedCodeBlock.Info != null) { - renderer.Write(fencedCodeBlock.UnescapedInfo); - } - if (fencedCodeBlock.WhitespaceAfterInfo != null) - { - renderer.Write(fencedCodeBlock.WhitespaceAfterInfo); + renderer.Write(fencedCodeBlock.Info); } if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) { - renderer.Write(fencedCodeBlock.UnescapedArguments); - } - if (fencedCodeBlock.WhitespaceAfterArguments != null) - { - renderer.Write(fencedCodeBlock.WhitespaceAfterArguments); + renderer.Write(" ").Write(fencedCodeBlock.Arguments); } /* TODO do we need this causes a empty space and would render html attributes to markdown. @@ -54,50 +37,17 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) renderer.Write(attributes); } */ - renderer.WriteLine(fencedCodeBlock.InfoNewline); - - renderer.WriteLeafRawLines(obj); + renderer.WriteLine(); - renderer.Write(fencedCodeBlock.WhitespaceBeforeClosingFence); - var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); - renderer.Write(closing); - if (!string.IsNullOrEmpty(closing)) - { - // See example 207: "> ```\nfoo\n```" - renderer.WriteLine(obj.Newline); - } - renderer.Write(obj.AfterWhitespace); + renderer.WriteLeafRawLines(obj, true); + renderer.Write(opening); } else { - var indents = new List(); - foreach (var cbl in obj.CodeBlockLines) - { - indents.Add(cbl.BeforeWhitespace.ToString()); - } - renderer.PushIndent(indents); - WriteLeafRawLines(renderer, obj); - renderer.PopIndent(); - - // ignore block newline, as last line references it + renderer.WriteLeafRawLines(obj, false, true); } - renderer.RenderLinesAfter(obj); - } - - public void WriteLeafRawLines(NormalizeRenderer renderer, LeafBlock leafBlock) - { - if (leafBlock.Lines.Lines != null) - { - var lines = leafBlock.Lines; - var slices = lines.Lines; - for (int i = 0; i < lines.Count; i++) - { - var slice = slices[i].Slice; - renderer.Write(ref slices[i].Slice); - renderer.WriteLine(slice.Newline); - } - } + renderer.FinishBlock(renderer.Options.EmptyLineAfterCodeBlock); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs index 85978cbd8..100901781 100644 --- a/src/Markdig/Renderers/Normalize/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HeadingRenderer.cs @@ -23,39 +23,14 @@ public class HeadingRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, HeadingBlock obj) { - if (obj.IsSetext) - { - renderer.RenderLinesBefore(obj); + var headingText = obj.Level > 0 && obj.Level <= 6 + ? HeadingTexts[obj.Level - 1] + : new string('#', obj.Level); - var headingChar = obj.Level == 1 ? '=' : '-'; - var line = new string(headingChar, obj.HeaderCharCount); + renderer.Write(headingText).Write(' '); + renderer.WriteLeafInline(obj); - renderer.WriteLeafInline(obj); - renderer.WriteLine(obj.SetextNewline); - renderer.Write(obj.BeforeWhitespace); - renderer.Write(line); - renderer.WriteLine(obj.Newline); - renderer.Write(obj.AfterWhitespace); - - renderer.RenderLinesAfter(obj); - } - else - { - renderer.RenderLinesBefore(obj); - - var headingText = obj.Level > 0 && obj.Level <= 6 - ? HeadingTexts[obj.Level - 1] - : new string('#', obj.Level); - - renderer.Write(obj.BeforeWhitespace); - renderer.Write(headingText); - renderer.Write(obj.WhitespaceAfterAtxHeaderChar); - renderer.WriteLeafInline(obj); - renderer.Write(obj.AfterWhitespace); - renderer.WriteLine(obj.Newline); - - renderer.RenderLinesAfter(obj); - } + renderer.FinishBlock(renderer.Options.EmptyLineAfterHeading); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs index cdeda8fcf..0cc59dc95 100644 --- a/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/HtmlBlockRenderer.cs @@ -10,10 +10,7 @@ public class HtmlBlockRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, HtmlBlock obj) { - renderer.RenderLinesBefore(obj); - //renderer.Write(obj.BeforeWhitespace); // Lines content is written, including whitespace - renderer.WriteLeafRawLines(obj); - renderer.RenderLinesAfter(obj); + renderer.WriteLeafRawLines(obj, true, false); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs index 327c97a97..46fd6b671 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/CodeInlineRenderer.cs @@ -14,11 +14,39 @@ public class CodeInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, CodeInline obj) { - var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); + var delimiterCount = 0; + for (var i = 0; i < obj.Content.Length; i++) + { + var index = obj.Content.IndexOf(obj.Delimiter, i); + if (index == -1) break; + + var count = 1; + for (i = index + 1; i < obj.Content.Length; i++) + { + if (obj.Content[i] == obj.Delimiter) count++; + else break; + } + + if (delimiterCount < count) + delimiterCount = count; + } + var delimiterRun = new string(obj.Delimiter, delimiterCount + 1); renderer.Write(delimiterRun); if (obj.Content.Length != 0) { - renderer.Write(obj.ContentWithTrivia); + if (obj.Content[0] == obj.Delimiter) + { + renderer.Write(' '); + } + renderer.Write(obj.Content); + if (obj.Content[obj.Content.Length - 1] == obj.Delimiter) + { + renderer.Write(' '); + } + } + else + { + renderer.Write(' '); } renderer.Write(delimiterRun); } diff --git a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs index 34893e6e1..ba3f94ed9 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LineBreakInlineRenderer.cs @@ -19,12 +19,11 @@ public class LineBreakInlineRenderer : NormalizeObjectRenderer protected override void Write(NormalizeRenderer renderer, LineBreakInline obj) { - if (obj.IsHard && obj.IsBackslash) + if (obj.IsHard) { - renderer.Write("\\"); - //renderer.Write(obj.IsBackslash ? "\\" : " "); + renderer.Write(obj.IsBackslash ? "\\" : " "); } - renderer.WriteLine(obj.Newline); + renderer.WriteLine(); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index a08a58c7b..1357ce93c 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -18,52 +18,37 @@ protected override void Write(NormalizeRenderer renderer, LinkInline link) { renderer.Write('!'); } - // link text renderer.Write('['); renderer.WriteChildren(link); renderer.Write(']'); if (link.Label != null) { - if (link.LocalLabel == LocalLabel.Local || link.LocalLabel == LocalLabel.Empty) + if (link.FirstChild is LiteralInline literal && literal.Content.Length == link.Label.Length && literal.Content.Match(link.Label)) { - renderer.Write('['); - if (link.LocalLabel == LocalLabel.Local) + // collapsed reference and shortcut links + if (!link.IsShortcut) { - renderer.Write(link.LabelWithWhitespace); + renderer.Write("[]"); } - renderer.Write(']'); + } + else + { + // full link + renderer.Write('[').Write(link.Label).Write(']'); } } else { - if (link.Url != null) + if (!string.IsNullOrEmpty(link.Url)) { - renderer.Write('('); - renderer.Write(link.WhitespaceBeforeUrl); - if (link.UrlHasPointyBrackets) - { - renderer.Write('<'); - } - renderer.Write(link.UnescapedUrl); - if (link.UrlHasPointyBrackets) - { - renderer.Write('>'); - } - renderer.Write(link.WhitespaceAfterUrl); + renderer.Write('(').Write(link.Url); - if (!string.IsNullOrEmpty(link.UnescapedTitle)) + if (!string.IsNullOrEmpty(link.Title)) { - var open = link.TitleEnclosingCharacter; - var close = link.TitleEnclosingCharacter; - if (link.TitleEnclosingCharacter == '(') - { - close = ')'; - } - renderer.Write(open); - renderer.Write(link.UnescapedTitle); - renderer.Write(close); - renderer.Write(link.WhitespaceAfterTitle); + renderer.Write(" \""); + renderer.Write(link.Title.Replace(@"""", @"\""")); + renderer.Write("\""); } renderer.Write(')'); diff --git a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs index a7c2dbf7b..8d1a7ca2d 100644 --- a/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs +++ b/src/Markdig/Renderers/Normalize/LinkReferenceDefinitionGroupRenderer.cs @@ -10,7 +10,9 @@ public class LinkReferenceDefinitionGroupRenderer : NormalizeObjectRenderer'); - } + renderer.Write(linkDef.Url); - renderer.Write(linkDef.WhitespaceBeforeTitle); - if (linkDef.UnescapedTitle != null) + if (linkDef.Title != null) { - var open = linkDef.TitleEnclosingCharacter; - var close = linkDef.TitleEnclosingCharacter; - if (linkDef.TitleEnclosingCharacter == '(') - { - close = ')'; - } - renderer.Write(open); - renderer.Write(linkDef.UnescapedTitle); - renderer.Write(close); + renderer.Write(" \""); + renderer.Write(linkDef.Title.Replace("\"", "\\\"")); + renderer.Write('"'); } - renderer.Write(linkDef.AfterWhitespace); - renderer.Write(linkDef.Newline); - - renderer.RenderLinesAfter(linkDef); + renderer.FinishBlock(false); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ListRenderer.cs b/src/Markdig/Renderers/Normalize/ListRenderer.cs index 2ef58330d..71b2c4749 100644 --- a/src/Markdig/Renderers/Normalize/ListRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ListRenderer.cs @@ -2,9 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Collections.Generic; using System.Globalization; -using Markdig.Helpers; using Markdig.Syntax; namespace Markdig.Renderers.Normalize @@ -17,7 +15,7 @@ public class ListRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) { - renderer.RenderLinesBefore(listBlock); + renderer.EnsureLine(); var compact = renderer.CompactParagraph; renderer.CompactParagraph = !listBlock.IsLoose; if (listBlock.IsOrdered) @@ -32,19 +30,13 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) break; } } - var writeLine = false; for (var i = 0; i < listBlock.Count; i++) { var item = listBlock[i]; var listItem = (ListItemBlock) item; - if (writeLine) - { - renderer.WriteLine(); - } + renderer.EnsureLine(); - renderer.Write(listItem.BeforeWhitespace); - //renderer.Write(index.ToString(CultureInfo.InvariantCulture)); - renderer.Write(listItem.SourceBullet); + renderer.Write(index.ToString(CultureInfo.InvariantCulture)); renderer.Write(listBlock.OrderedDelimiter); renderer.Write(' '); renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); @@ -58,10 +50,9 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) } if (i + 1 < listBlock.Count && listBlock.IsLoose) { - //renderer.EnsureLine(); + renderer.EnsureLine(); renderer.WriteLine(); } - writeLine = true; } } else @@ -70,30 +61,22 @@ protected override void Write(NormalizeRenderer renderer, ListBlock listBlock) { var item = listBlock[i]; var listItem = (ListItemBlock) item; - renderer.RenderLinesBefore(listItem); - - StringSlice bws = listItem.BeforeWhitespace; - char bullet = renderer.Options.ListItemCharacter ?? listBlock.BulletType; - StringSlice aws = listItem.AfterWhitespace; - - renderer.PushIndent(new List { $@"{bws}{bullet}{aws}" }); - if (listItem.Count == 0) - { - renderer.Write(""); // trigger writing of indent - } - else + renderer.EnsureLine(); + renderer.Write(renderer.Options.ListItemCharacter ?? listBlock.BulletType); + renderer.Write(' '); + renderer.PushIndent(" "); + renderer.WriteChildren(listItem); + renderer.PopIndent(); + if (i + 1 < listBlock.Count && listBlock.IsLoose) { - renderer.WriteChildren(listItem); + renderer.EnsureLine(); + renderer.WriteLine(); } - renderer.PopIndent(); - - renderer.RenderLinesAfter(listItem); } } renderer.CompactParagraph = compact; - - renderer.RenderLinesAfter(listBlock); + renderer.FinishBlock(true); } diff --git a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs index 83f9ff3ac..456eaf195 100644 --- a/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs +++ b/src/Markdig/Renderers/Normalize/NormalizeRenderer.cs @@ -50,6 +50,18 @@ public NormalizeRenderer(TextWriter writer, NormalizeOptions options = null) : b public bool CompactParagraph { get; set; } + public void FinishBlock(bool emptyLine) + { + if (!IsLastInContainer) + { + WriteLine(); + if (emptyLine) + { + WriteLine(); + } + } + } + ///// ///// Writes the attached on the specified . ///// @@ -117,7 +129,7 @@ public NormalizeRenderer(TextWriter writer, NormalizeOptions options = null) : b /// if set to true write end of lines. /// Whether to write indents. /// This instance - public void WriteLeafRawLines(LeafBlock leafBlock) + public NormalizeRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool indent = false) { if (leafBlock == null) ThrowHelper.ArgumentNullException_leafBlock(); if (leafBlock.Lines.Lines != null) @@ -126,38 +138,25 @@ public void WriteLeafRawLines(LeafBlock leafBlock) var slices = lines.Lines; for (int i = 0; i < lines.Count; i++) { - var slice = slices[i].Slice; - Write(ref slice); - WriteLine(slice.Newline); - } - } - } + if (!writeEndOfLines && i > 0) + { + WriteLine(); + } - public void RenderLinesBefore(Block block) - { - if (block.LinesBefore == null) - { - return; - } - foreach (var line in block.LinesBefore) - { - Write(line); - WriteLine(line.Newline); - } - } + if (indent) + { + Write(" "); + } - public void RenderLinesAfter(Block block) - { - previousWasLine = true; - if (block.LinesAfter == null) - { - return; - } - foreach (var line in block.LinesAfter) - { - Write(line); - WriteLine(line.Newline); + Write(ref slices[i].Slice); + + if (writeEndOfLines) + { + WriteLine(); + } + } } + return this; } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs index 5b5fc7526..6fa0f65af 100644 --- a/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ParagraphRenderer.cs @@ -3,7 +3,6 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; -using System.Diagnostics; namespace Markdig.Renderers.Normalize { @@ -11,16 +10,12 @@ namespace Markdig.Renderers.Normalize /// A Normalize renderer for a . /// /// - [DebuggerDisplay("renderer.Writer.ToString()")] public class ParagraphRenderer : NormalizeObjectRenderer { - protected override void Write(NormalizeRenderer renderer, ParagraphBlock paragraph) + protected override void Write(NormalizeRenderer renderer, ParagraphBlock obj) { - renderer.RenderLinesBefore(paragraph); - renderer.Write(paragraph.BeforeWhitespace); - renderer.WriteLeafInline(paragraph); - //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes - renderer.RenderLinesAfter(paragraph); + renderer.WriteLeafInline(obj); + renderer.FinishBlock(!renderer.CompactParagraph); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs index d78aae9ba..345b99341 100644 --- a/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/QuoteBlockRenderer.cs @@ -3,8 +3,6 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; -using Markdig.Syntax.Inlines; -using System.Collections.Generic; namespace Markdig.Renderers.Normalize { @@ -14,51 +12,14 @@ namespace Markdig.Renderers.Normalize /// public class QuoteBlockRenderer : NormalizeObjectRenderer { - protected override void Write(NormalizeRenderer renderer, QuoteBlock quoteBlock) + protected override void Write(NormalizeRenderer renderer, QuoteBlock obj) { - renderer.RenderLinesBefore(quoteBlock); - renderer.Write(quoteBlock.BeforeWhitespace); - - var indents = new List(); - foreach (var quoteLine in quoteBlock.QuoteLines) - { - var wsb = quoteLine.BeforeWhitespace.ToString(); - var quoteChar = quoteLine.QuoteChar ? ">" : ""; - var wsa = quoteLine.AfterWhitespace.ToString(); - indents.Add(wsb + quoteChar + wsa); - } - bool noChildren = false; - if (quoteBlock.Count == 0) - { - noChildren = true; - // since this QuoteBlock instance has no children, indents will not be rendered. We - // work around this by adding empty LineBreakInlines to a ParagraphBlock. - // Wanted: a more elegant/better solution (although this is not *that* bad). - foreach (var quoteLine in quoteBlock.QuoteLines) - { - var emptyLeafBlock = new ParagraphBlock - { - Newline = quoteLine.Newline - }; - var newline = new LineBreakInline - { - Newline = quoteLine.Newline - }; - var container = new ContainerInline(); - container.AppendChild(newline); - emptyLeafBlock.Inline = container; - quoteBlock.Add(emptyLeafBlock); - } - } - - renderer.PushIndent(indents); - renderer.WriteChildren(quoteBlock); + var quoteIndent = renderer.Options.SpaceAfterQuoteBlock ? obj.QuoteChar + " " : obj.QuoteChar.ToString(); + renderer.PushIndent(quoteIndent); + renderer.WriteChildren(obj); renderer.PopIndent(); - if (!noChildren) - { - renderer.RenderLinesAfter(quoteBlock); - } + renderer.FinishBlock(true); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs b/src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs deleted file mode 100644 index a552712c4..000000000 --- a/src/Markdig/Renderers/Normalize/SetextHeadingRenderer.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Markdig.Renderers.Normalize -{ - class SetextHeadingRenderer - { - } -} diff --git a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs index 563b387d7..1202b098d 100644 --- a/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Normalize/ThematicBreakRenderer.cs @@ -2,7 +2,6 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using Markdig.Helpers; using Markdig.Syntax; namespace Markdig.Renderers.Normalize @@ -15,15 +14,9 @@ public class ThematicBreakRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, ThematicBreakBlock obj) { - renderer.RenderLinesBefore(obj); + renderer.WriteLine(new string(obj.ThematicChar, obj.ThematicCharCount)); - // for now, render always a newline - // TODO: only render a newline when not last line - //renderer.Write(obj.BeforeWhitespace); - renderer.Write(obj.Content); - //renderer.Write(obj.AfterWhitespace); - renderer.WriteLine(obj.Newline); - renderer.RenderLinesAfter(obj); + renderer.FinishBlock(renderer.Options.EmptyLineAfterThematicBreak); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs new file mode 100644 index 000000000..40cbfdc42 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -0,0 +1,102 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; +using System.Collections.Generic; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// An Normalize renderer for a and . + /// + /// + public class CodeBlockRenderer : RoundtripObjectRenderer + { + public bool OutputAttributesOnPre { get; set; } + + protected override void Write(RoundtripRenderer renderer, CodeBlock obj) + { + renderer.RenderLinesBefore(obj); + if (obj is FencedCodeBlock fencedCodeBlock) + { + renderer.Write(obj.BeforeWhitespace); + var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); + renderer.Write(opening); + + if (fencedCodeBlock.WhitespaceAfterFencedChar != null) + { + renderer.Write(fencedCodeBlock.WhitespaceAfterFencedChar); + } + if (fencedCodeBlock.Info != null) + { + renderer.Write(fencedCodeBlock.UnescapedInfo); + } + if (fencedCodeBlock.WhitespaceAfterInfo != null) + { + renderer.Write(fencedCodeBlock.WhitespaceAfterInfo); + } + if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) + { + renderer.Write(fencedCodeBlock.UnescapedArguments); + } + if (fencedCodeBlock.WhitespaceAfterArguments != null) + { + renderer.Write(fencedCodeBlock.WhitespaceAfterArguments); + } + + /* TODO do we need this causes a empty space and would render html attributes to markdown. + var attributes = obj.TryGetAttributes(); + if (attributes != null) + { + renderer.Write(" "); + renderer.Write(attributes); + } + */ + renderer.WriteLine(fencedCodeBlock.InfoNewline); + + renderer.WriteLeafRawLines(obj); + + renderer.Write(fencedCodeBlock.WhitespaceBeforeClosingFence); + var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); + renderer.Write(closing); + if (!string.IsNullOrEmpty(closing)) + { + // See example 207: "> ```\nfoo\n```" + renderer.WriteLine(obj.Newline); + } + renderer.Write(obj.AfterWhitespace); + } + else + { + var indents = new List(); + foreach (var cbl in obj.CodeBlockLines) + { + indents.Add(cbl.BeforeWhitespace.ToString()); + } + renderer.PushIndent(indents); + WriteLeafRawLines(renderer, obj); + renderer.PopIndent(); + + // ignore block newline, as last line references it + } + + renderer.RenderLinesAfter(obj); + } + + public void WriteLeafRawLines(RoundtripRenderer renderer, LeafBlock leafBlock) + { + if (leafBlock.Lines.Lines != null) + { + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (int i = 0; i < lines.Count; i++) + { + var slice = slices[i].Slice; + renderer.Write(ref slices[i].Slice); + renderer.WriteLine(slice.Newline); + } + } + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs new file mode 100644 index 000000000..55240ba46 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs @@ -0,0 +1,61 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// An Normalize renderer for a . + /// + /// + public class HeadingRenderer : RoundtripObjectRenderer + { + private static readonly string[] HeadingTexts = { + "#", + "##", + "###", + "####", + "#####", + "######", + }; + + protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) + { + if (obj.IsSetext) + { + renderer.RenderLinesBefore(obj); + + var headingChar = obj.Level == 1 ? '=' : '-'; + var line = new string(headingChar, obj.HeaderCharCount); + + renderer.WriteLeafInline(obj); + renderer.WriteLine(obj.SetextNewline); + renderer.Write(obj.BeforeWhitespace); + renderer.Write(line); + renderer.WriteLine(obj.Newline); + renderer.Write(obj.AfterWhitespace); + + renderer.RenderLinesAfter(obj); + } + else + { + renderer.RenderLinesBefore(obj); + + var headingText = obj.Level > 0 && obj.Level <= 6 + ? HeadingTexts[obj.Level - 1] + : new string('#', obj.Level); + + renderer.Write(obj.BeforeWhitespace); + renderer.Write(headingText); + renderer.Write(obj.WhitespaceAfterAtxHeaderChar); + renderer.WriteLeafInline(obj); + renderer.Write(obj.AfterWhitespace); + renderer.WriteLine(obj.Newline); + + renderer.RenderLinesAfter(obj); + } + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs new file mode 100644 index 000000000..07712a7f9 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/HtmlBlockRenderer.cs @@ -0,0 +1,19 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + public class HtmlBlockRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, HtmlBlock obj) + { + renderer.RenderLinesBefore(obj); + //renderer.Write(obj.BeforeWhitespace); // Lines content is written, including whitespace + renderer.WriteLeafRawLines(obj); + renderer.RenderLinesAfter(obj); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs new file mode 100644 index 000000000..8a53ad206 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/AutolinkInlineRenderer.cs @@ -0,0 +1,20 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for an . + /// + /// + public class AutolinkInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, AutolinkInline obj) + { + renderer.Write('<').Write(obj.Url).Write('>'); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs new file mode 100644 index 000000000..6dd570a13 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/CodeInlineRenderer.cs @@ -0,0 +1,26 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + /// + public class CodeInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, CodeInline obj) + { + var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount); + renderer.Write(delimiterRun); + if (obj.Content.Length != 0) + { + renderer.Write(obj.ContentWithTrivia); + } + renderer.Write(delimiterRun); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs new file mode 100644 index 000000000..bd2804c36 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/DelimiterInlineRenderer.cs @@ -0,0 +1,21 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + /// + public class DelimiterInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, DelimiterInline obj) + { + renderer.Write(obj.ToLiteral()); + renderer.WriteChildren(obj); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs new file mode 100644 index 000000000..8a8a11977 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/EmphasisInlineRenderer.cs @@ -0,0 +1,23 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for an . + /// + /// + public class EmphasisInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, EmphasisInline obj) + { + var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount); + renderer.Write(emphasisText); + renderer.WriteChildren(obj); + renderer.Write(emphasisText); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs new file mode 100644 index 000000000..a6bdab6d2 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs @@ -0,0 +1,30 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + /// + public class LineBreakInlineRenderer : RoundtripObjectRenderer + { + /// + /// Gets or sets a value indicating whether to render this softline break as a Normalize hardline break tag (<br />) + /// + public bool RenderAsHardlineBreak { get; set; } + + protected override void Write(RoundtripRenderer renderer, LineBreakInline obj) + { + if (obj.IsHard && obj.IsBackslash) + { + renderer.Write("\\"); + //renderer.Write(obj.IsBackslash ? "\\" : " "); + } + renderer.WriteLine(obj.Newline); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs new file mode 100644 index 000000000..62fd46698 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs @@ -0,0 +1,74 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + /// + public class LinkInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, LinkInline link) + { + if (link.IsImage) + { + renderer.Write('!'); + } + // link text + renderer.Write('['); + renderer.WriteChildren(link); + renderer.Write(']'); + + if (link.Label != null) + { + if (link.LocalLabel == LocalLabel.Local || link.LocalLabel == LocalLabel.Empty) + { + renderer.Write('['); + if (link.LocalLabel == LocalLabel.Local) + { + renderer.Write(link.LabelWithWhitespace); + } + renderer.Write(']'); + } + } + else + { + if (link.Url != null) + { + renderer.Write('('); + renderer.Write(link.WhitespaceBeforeUrl); + if (link.UrlHasPointyBrackets) + { + renderer.Write('<'); + } + renderer.Write(link.UnescapedUrl); + if (link.UrlHasPointyBrackets) + { + renderer.Write('>'); + } + renderer.Write(link.WhitespaceAfterUrl); + + if (!string.IsNullOrEmpty(link.UnescapedTitle)) + { + var open = link.TitleEnclosingCharacter; + var close = link.TitleEnclosingCharacter; + if (link.TitleEnclosingCharacter == '(') + { + close = ')'; + } + renderer.Write(open); + renderer.Write(link.UnescapedTitle); + renderer.Write(close); + renderer.Write(link.WhitespaceAfterTitle); + } + + renderer.Write(')'); + } + } + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs new file mode 100644 index 000000000..f6a43bb88 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LiteralInlineRenderer.cs @@ -0,0 +1,25 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Helpers; +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + /// + public class LiteralInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, LiteralInline obj) + { + if (obj.IsFirstCharacterEscaped && obj.Content.Length > 0 && obj.Content[obj.Content.Start].IsAsciiPunctuation()) + { + renderer.Write('\\'); + } + renderer.Write(ref obj.Content); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs new file mode 100644 index 000000000..22fcd9900 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlEntityInlineRenderer.cs @@ -0,0 +1,19 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + public class RoundtripHtmlEntityInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, HtmlEntityInline obj) + { + renderer.Write(obj.Original); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs new file mode 100644 index 000000000..75548d869 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/Inlines/RoundtripHtmlInlineRenderer.cs @@ -0,0 +1,19 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace Markdig.Renderers.Roundtrip.Inlines +{ + /// + /// A Normalize renderer for a . + /// + public class RoundtripHtmlInlineRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, HtmlInline obj) + { + renderer.Write(obj.Tag); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs new file mode 100644 index 000000000..3aa1c07da --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionGroupRenderer.cs @@ -0,0 +1,16 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + public class LinkReferenceDefinitionGroupRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitionGroup obj) + { + renderer.WriteChildren(obj); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs new file mode 100644 index 000000000..63c855360 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -0,0 +1,50 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + public class LinkReferenceDefinitionRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinition linkDef) + { + renderer.RenderLinesBefore(linkDef); + + renderer.Write(linkDef.BeforeWhitespace); + renderer.Write('['); + renderer.Write(linkDef.LabelWithWhitespace); + renderer.Write("]:"); + + renderer.Write(linkDef.WhitespaceBeforeUrl); + if (linkDef.UrlHasPointyBrackets) + { + renderer.Write('<'); + } + renderer.Write(linkDef.UnescapedUrl); + if (linkDef.UrlHasPointyBrackets) + { + renderer.Write('>'); + } + + renderer.Write(linkDef.WhitespaceBeforeTitle); + if (linkDef.UnescapedTitle != null) + { + var open = linkDef.TitleEnclosingCharacter; + var close = linkDef.TitleEnclosingCharacter; + if (linkDef.TitleEnclosingCharacter == '(') + { + close = ')'; + } + renderer.Write(open); + renderer.Write(linkDef.UnescapedTitle); + renderer.Write(close); + } + renderer.Write(linkDef.AfterWhitespace); + renderer.Write(linkDef.Newline); + + renderer.RenderLinesAfter(linkDef); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs new file mode 100644 index 000000000..70f87d383 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -0,0 +1,110 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System.Collections.Generic; +using Markdig.Helpers; +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// A Normalize renderer for a . + /// + /// + public class ListRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) + { + renderer.RenderLinesBefore(listBlock); + var compact = renderer.CompactParagraph; + renderer.CompactParagraph = !listBlock.IsLoose; + if (listBlock.IsOrdered) + { + int index = 0; + if (listBlock.OrderedStart != null) + { + switch (listBlock.BulletType) + { + case '1': + int.TryParse(listBlock.OrderedStart, out index); + break; + } + } + var writeLine = false; + for (var i = 0; i < listBlock.Count; i++) + { + var item = listBlock[i]; + var listItem = (ListItemBlock) item; + if (writeLine) + { + renderer.WriteLine(); + } + + renderer.Write(listItem.BeforeWhitespace); + //renderer.Write(index.ToString(CultureInfo.InvariantCulture)); + renderer.Write(listItem.SourceBullet); + renderer.Write(listBlock.OrderedDelimiter); + renderer.Write(' '); + renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); + renderer.WriteChildren(listItem); + renderer.PopIndent(); + switch (listBlock.BulletType) + { + case '1': + index++; + break; + } + if (i + 1 < listBlock.Count && listBlock.IsLoose) + { + //renderer.EnsureLine(); + renderer.WriteLine(); + } + writeLine = true; + } + } + else + { + for (var i = 0; i < listBlock.Count; i++) + { + var item = listBlock[i]; + var listItem = (ListItemBlock) item; + renderer.RenderLinesBefore(listItem); + + StringSlice bws = listItem.BeforeWhitespace; + char bullet = listBlock.BulletType; + StringSlice aws = listItem.AfterWhitespace; + + renderer.PushIndent(new List { $@"{bws}{bullet}{aws}" }); + if (listItem.Count == 0) + { + renderer.Write(""); // trigger writing of indent + } + else + { + renderer.WriteChildren(listItem); + } + renderer.PopIndent(); + + renderer.RenderLinesAfter(listItem); + } + } + renderer.CompactParagraph = compact; + + + renderer.RenderLinesAfter(listBlock); + } + + + private static int IntLog10Fast(int input) => + (input < 10) ? 0 : + (input < 100) ? 1 : + (input < 1000) ? 2 : + (input < 10000) ? 3 : + (input < 100000) ? 4 : + (input < 1000000) ? 5 : + (input < 10000000) ? 6 : + (input < 100000000) ? 7 : + (input < 1000000000) ? 8 : 9; + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs new file mode 100644 index 000000000..d387d321f --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs @@ -0,0 +1,26 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; +using System.Diagnostics; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// A Normalize renderer for a . + /// + /// + [DebuggerDisplay("renderer.Writer.ToString()")] + public class ParagraphRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, ParagraphBlock paragraph) + { + renderer.RenderLinesBefore(paragraph); + renderer.Write(paragraph.BeforeWhitespace); + renderer.WriteLeafInline(paragraph); + //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes + renderer.RenderLinesAfter(paragraph); + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs new file mode 100644 index 000000000..b2b627906 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -0,0 +1,64 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using System.Collections.Generic; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// A Normalize renderer for a . + /// + /// + public class QuoteBlockRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) + { + renderer.RenderLinesBefore(quoteBlock); + renderer.Write(quoteBlock.BeforeWhitespace); + + var indents = new List(); + foreach (var quoteLine in quoteBlock.QuoteLines) + { + var wsb = quoteLine.BeforeWhitespace.ToString(); + var quoteChar = quoteLine.QuoteChar ? ">" : ""; + var wsa = quoteLine.AfterWhitespace.ToString(); + indents.Add(wsb + quoteChar + wsa); + } + bool noChildren = false; + if (quoteBlock.Count == 0) + { + noChildren = true; + // since this QuoteBlock instance has no children, indents will not be rendered. We + // work around this by adding empty LineBreakInlines to a ParagraphBlock. + // Wanted: a more elegant/better solution (although this is not *that* bad). + foreach (var quoteLine in quoteBlock.QuoteLines) + { + var emptyLeafBlock = new ParagraphBlock + { + Newline = quoteLine.Newline + }; + var newline = new LineBreakInline + { + Newline = quoteLine.Newline + }; + var container = new ContainerInline(); + container.AppendChild(newline); + emptyLeafBlock.Inline = container; + quoteBlock.Add(emptyLeafBlock); + } + } + + renderer.PushIndent(indents); + renderer.WriteChildren(quoteBlock); + renderer.PopIndent(); + + if (!noChildren) + { + renderer.RenderLinesAfter(quoteBlock); + } + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs new file mode 100644 index 000000000..0f8d7f95c --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/RoundtripObjectRenderer.cs @@ -0,0 +1,17 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// A base class for Normalize rendering and Markdown objects. + /// + /// The type of the object. + /// + public abstract class RoundtripObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject + { + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs new file mode 100644 index 000000000..952a882da --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -0,0 +1,159 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System.IO; +using Markdig.Syntax; +using Markdig.Renderers.Roundtrip.Inlines; +using Markdig.Helpers; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// Default HTML renderer for a Markdown object. + /// + public class RoundtripRenderer : TextRendererBase + { + /// + /// Initializes a new instance of the class. + /// + /// The writer. + /// The normalize options + public RoundtripRenderer(TextWriter writer) : base(writer) + { + // Default block renderers + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new HtmlBlockRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteBlockRenderer()); + ObjectRenderers.Add(new ThematicBreakRenderer()); + ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); + ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); + + // Default inline renderers + ObjectRenderers.Add(new AutolinkInlineRenderer()); + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new RoundtripHtmlInlineRenderer()); + ObjectRenderers.Add(new RoundtripHtmlEntityInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + } + + public bool CompactParagraph { get; set; } + + ///// + ///// Writes the attached on the specified . + ///// + ///// The object. + ///// + //public NormalizeRenderer WriteAttributes(MarkdownObject obj) + //{ + // if (obj == null) throw new ArgumentNullException(nameof(obj)); + // return WriteAttributes(obj.TryGetAttributes()); + //} + + ///// + ///// Writes the specified . + ///// + ///// The attributes to render. + ///// This instance + //public NormalizeRenderer WriteAttributes(HtmlAttributes attributes) + //{ + // if (attributes == null) + // { + // return this; + // } + + // if (attributes.Id != null) + // { + // Write(" id=\"").WriteEscape(attributes.Id).Write("\""); + // } + + // if (attributes.Classes != null && attributes.Classes.Count > 0) + // { + // Write(" class=\""); + // for (int i = 0; i < attributes.Classes.Count; i++) + // { + // var cssClass = attributes.Classes[i]; + // if (i > 0) + // { + // Write(" "); + // } + // WriteEscape(cssClass); + // } + // Write("\""); + // } + + // if (attributes.Properties != null && attributes.Properties.Count > 0) + // { + // foreach (var property in attributes.Properties) + // { + // Write(" ").Write(property.Key); + // if (property.Value != null) + // { + // Write("=").Write("\""); + // WriteEscape(property.Value); + // Write("\""); + // } + // } + // } + + // return this; + //} + + /// + /// Writes the lines of a + /// + /// The leaf block. + /// if set to true write end of lines. + /// Whether to write indents. + /// This instance + public void WriteLeafRawLines(LeafBlock leafBlock) + { + if (leafBlock == null) ThrowHelper.ArgumentNullException_leafBlock(); + if (leafBlock.Lines.Lines != null) + { + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (int i = 0; i < lines.Count; i++) + { + var slice = slices[i].Slice; + Write(ref slice); + WriteLine(slice.Newline); + } + } + } + + public void RenderLinesBefore(Block block) + { + if (block.LinesBefore == null) + { + return; + } + foreach (var line in block.LinesBefore) + { + Write(line); + WriteLine(line.Newline); + } + } + + public void RenderLinesAfter(Block block) + { + previousWasLine = true; + if (block.LinesAfter == null) + { + return; + } + foreach (var line in block.LinesAfter) + { + Write(line); + WriteLine(line.Newline); + } + } + } +} \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs new file mode 100644 index 000000000..2460b949b --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs @@ -0,0 +1,28 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + /// + /// A Normalize renderer for a . + /// + /// + public class ThematicBreakRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, ThematicBreakBlock obj) + { + renderer.RenderLinesBefore(obj); + + // for now, render always a newline + // TODO: only render a newline when not last line + //renderer.Write(obj.BeforeWhitespace); + renderer.Write(obj.Content); + //renderer.Write(obj.AfterWhitespace); + renderer.WriteLine(obj.Newline); + renderer.RenderLinesAfter(obj); + } + } +} \ No newline at end of file From c8da4301345db902f00e0e35efd06dff8ab221c8 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 15:48:19 +0100 Subject: [PATCH 083/120] cleanup RoundtripRenderer --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 4 +- .../Renderers/Normalize/CodeBlockRenderer.cs | 4 +- .../Renderers/Roundtrip/ListRenderer.cs | 4 -- .../Renderers/Roundtrip/RoundtripRenderer.cs | 62 ------------------- 4 files changed, 5 insertions(+), 69 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 8305b0eb5..9398a858c 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -38,8 +38,8 @@ In order: - ~~check char.IsNewline() calls~~ - ~~introduce feature flag~~ - ~~extract MarkdownRenderer~~ -- cleanup NormalizeRenderer (MarkdownRenderer) -- deduplicate MarkdownRenderer and NormalizeRenderer code +- ~~cleanup NormalizeRenderer (MarkdownRenderer)~~ +- ~~deduplicate MarkdownRenderer and NormalizeRenderer code~~ - do pull request feedback - split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? - fix broken pre-existing tests diff --git a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs index c9cb26136..811b440d3 100644 --- a/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Normalize/CodeBlockRenderer.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; +using System; namespace Markdig.Renderers.Normalize { @@ -18,7 +19,8 @@ protected override void Write(NormalizeRenderer renderer, CodeBlock obj) { if (obj is FencedCodeBlock fencedCodeBlock) { - var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.FencedCharCount); + var fencedCharCount = Math.Min(fencedCodeBlock.OpeningFencedCharCount, fencedCodeBlock.ClosingFencedCharCount); + var opening = new string(fencedCodeBlock.FencedChar, fencedCharCount); renderer.Write(opening); if (fencedCodeBlock.Info != null) { diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index 70f87d383..360866a7d 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -17,8 +17,6 @@ public class ListRenderer : RoundtripObjectRenderer protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) { renderer.RenderLinesBefore(listBlock); - var compact = renderer.CompactParagraph; - renderer.CompactParagraph = !listBlock.IsLoose; if (listBlock.IsOrdered) { int index = 0; @@ -89,8 +87,6 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) renderer.RenderLinesAfter(listItem); } } - renderer.CompactParagraph = compact; - renderer.RenderLinesAfter(listBlock); } diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs index 952a882da..12b732966 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -44,68 +44,6 @@ public RoundtripRenderer(TextWriter writer) : base(writer) ObjectRenderers.Add(new LiteralInlineRenderer()); } - public bool CompactParagraph { get; set; } - - ///// - ///// Writes the attached on the specified . - ///// - ///// The object. - ///// - //public NormalizeRenderer WriteAttributes(MarkdownObject obj) - //{ - // if (obj == null) throw new ArgumentNullException(nameof(obj)); - // return WriteAttributes(obj.TryGetAttributes()); - //} - - ///// - ///// Writes the specified . - ///// - ///// The attributes to render. - ///// This instance - //public NormalizeRenderer WriteAttributes(HtmlAttributes attributes) - //{ - // if (attributes == null) - // { - // return this; - // } - - // if (attributes.Id != null) - // { - // Write(" id=\"").WriteEscape(attributes.Id).Write("\""); - // } - - // if (attributes.Classes != null && attributes.Classes.Count > 0) - // { - // Write(" class=\""); - // for (int i = 0; i < attributes.Classes.Count; i++) - // { - // var cssClass = attributes.Classes[i]; - // if (i > 0) - // { - // Write(" "); - // } - // WriteEscape(cssClass); - // } - // Write("\""); - // } - - // if (attributes.Properties != null && attributes.Properties.Count > 0) - // { - // foreach (var property in attributes.Properties) - // { - // Write(" ").Write(property.Key); - // if (property.Value != null) - // { - // Write("=").Write("\""); - // WriteEscape(property.Value); - // Write("\""); - // } - // } - // } - - // return this; - //} - /// /// Writes the lines of a /// From ab5e8ae9e2a6e41af36dcf37bc30d94e9b90842a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 16:12:06 +0100 Subject: [PATCH 084/120] apply review feedback --- .../RoundtripSpecs/Inlines/TestLinkInline.cs | 2 +- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 1 + .../RoundtripSpecs/TestFencedCodeBlock.cs | 19 +++++ src/Markdig.Tests/TestFencedCodeBlock.cs | 69 ------------------- src/Markdig.Tests/TestHtmlAttributes.cs | 2 +- src/Markdig/Helpers/LineReader.cs | 5 -- src/Markdig/Parsers/BlockProcessor.cs | 13 ++-- src/Markdig/Parsers/FencedBlockParserBase.cs | 8 +-- src/Markdig/Parsers/FencedCodeBlockParser.cs | 2 +- src/Markdig/Parsers/HeadingBlockParser.cs | 2 +- .../Parsers/IndentedCodeBlockParser.cs | 4 +- src/Markdig/Parsers/ListBlockParser.cs | 2 +- src/Markdig/Parsers/ParagraphBlockParser.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 10 +-- src/Markdig/Syntax/Block.cs | 2 +- src/Markdig/Syntax/QuoteBlock.cs | 26 +++---- 16 files changed, 60 insertions(+), 109 deletions(-) delete mode 100644 src/Markdig.Tests/TestFencedCodeBlock.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs index 4322075d0..a3a39ec20 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -34,7 +34,7 @@ public class TestLinkInline [TestCase("[ a ](b) ")] [TestCase(" [ a ](b) ")] - // below cases are required for a full CST but not have low prio for impl + // below cases are required for a full roundtrip but not have low prio for impl [TestCase("[]( b)")] [TestCase(" []( b)")] [TestCase("[]( b) ")] diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 9398a858c..364adfe6a 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -43,6 +43,7 @@ In order: - do pull request feedback - split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? - fix broken pre-existing tests +- document newly added syntax properties - support extensions - review complete PR and follow conventions - run perf test diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index 317dd9014..b7d8047eb 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -78,5 +78,24 @@ public void TestNewline(string value) { RoundTrip(value); } + + [TestCase("```\ni a\n```")] + [TestCase("```\ni a a2\n```")] + [TestCase("```\ni a a2 a3\n```")] + [TestCase("```\ni a a2 a3 a4\n```")] + + [TestCase("```\ni\ta\n```")] + [TestCase("```\ni\ta a2\n```")] + [TestCase("```\ni\ta a2 a3\n```")] + [TestCase("```\ni\ta a2 a3 a4\n```")] + + [TestCase("```\ni\ta \n```")] + [TestCase("```\ni\ta a2 \n```")] + [TestCase("```\ni\ta a2 a3 \n```")] + [TestCase("```\ni\ta a2 a3 a4 \n```")] + public void TestInfoArguments(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/TestFencedCodeBlock.cs b/src/Markdig.Tests/TestFencedCodeBlock.cs deleted file mode 100644 index 180339125..000000000 --- a/src/Markdig.Tests/TestFencedCodeBlock.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Markdig.Renderers.Normalize; -using Markdig.Renderers.Roundtrip; -using Markdig.Syntax; -using NUnit.Framework; -using System.IO; - -namespace Markdig.Tests -{ - // TODO: RTP: integrate with roundtrip synthetic tests - [TestFixture] - public partial class TestFencedCodeBlock - { - [Test] - public void CstInfoParser_ParsesHappyFlow() - { - string markdown = $@" -``` derpy args -``` -"; - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var fencedCodeBlock = markdownDocument[0] as FencedCodeBlock; - - Assert.AreEqual('`', fencedCodeBlock.FencedChar); - Assert.AreEqual(3, fencedCodeBlock.OpeningFencedCharCount); - Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterFencedChar); - Assert.AreEqual("derpy", fencedCodeBlock.Info); - Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterInfo); - Assert.AreEqual("args", fencedCodeBlock.Arguments); - Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterArguments); - } - - [Test] - public void CstInfoParser_ParsesComplicatedArguments() - { - string markdown = "``` derpy args more\t args \t\t and even more args \t \r\n```"; - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var fencedCodeBlock = markdownDocument[0] as FencedCodeBlock; - - Assert.AreEqual('`', fencedCodeBlock.FencedChar); - Assert.AreEqual(3, fencedCodeBlock.OpeningFencedCharCount); - Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterFencedChar); - Assert.AreEqual("derpy", fencedCodeBlock.Info); - Assert.AreEqual(" ", fencedCodeBlock.WhitespaceAfterInfo); - Assert.AreEqual("args more\t args \t\t and even more args", fencedCodeBlock.Arguments); - Assert.AreEqual(" \t ", fencedCodeBlock.WhitespaceAfterArguments); - } - - [Test] - public void CstInfoParser_RoundTrip() - { - string markdown = "``` derpy args more\t args \t\t and even more args \t \n```"; - var pipelineBuilder = new MarkdownPipelineBuilder(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var nr = new RoundtripRenderer(sw); - nr.Write(markdownDocument); - - Assert.AreEqual(markdown, sw.ToString()); - } - } -} diff --git a/src/Markdig.Tests/TestHtmlAttributes.cs b/src/Markdig.Tests/TestHtmlAttributes.cs index 541f92bc3..2fe2895cd 100644 --- a/src/Markdig.Tests/TestHtmlAttributes.cs +++ b/src/Markdig.Tests/TestHtmlAttributes.cs @@ -1,4 +1,4 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. +// Copyright (c) Alexandre Mutel. All rights reserved. // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index 20ebfd5ab..3996edfa9 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -33,11 +33,6 @@ public LineReader(string text) /// public int SourcePosition { get; private set; } - public bool IsEndOfFile() - { - return SourcePosition > _text.Length; - } - /// /// Reads a new line from the underlying and update the for the next line. /// diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index ff5ade428..3689610ec 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -170,7 +170,13 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo public int WhitespaceStart { get; set; } - public StringSlice PopBeforeWhitespace(int end) + /// + /// Returns trivia that has not yet been assigned to any node and + /// advances the position of trivia to the ending position + /// + /// End position of the trivia + /// + public StringSlice UseWhitespace(int end) { // NOTE: Line.Text is full source, not the line (!) var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); @@ -818,10 +824,9 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (TrackTrivia) { // TODO: RTP: delegate this to container parser classes - var qb = paragraph.Parent as QuoteBlock; - if (qb != null) + if (paragraph.Parent is QuoteBlock qb) { - var afterWhitespace = PopBeforeWhitespace(Start - 1); + var afterWhitespace = UseWhitespace(Start - 1); qb.QuoteLines.Last().AfterWhitespace = afterWhitespace; } } diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index d25b842a5..5f29c3c65 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -44,7 +44,7 @@ public abstract class FencedBlockParserBase : FencedBlockParserBase where T : /// protected FencedBlockParserBase() { - InfoParser = CstInfoParser; + InfoParser = RoundtripInfoParser; MinimumMatchCount = 3; MaximumMatchCount = int.MaxValue; } @@ -68,14 +68,14 @@ private enum ParseState } /// - /// The CST parser for the information after the fenced code block special characters (usually ` or ~) + /// The roundtrip parser for the information after the fenced code block special characters (usually ` or ~) /// /// The parser processor. /// The line. /// The fenced code block. /// The opening character for this fenced code block. /// true if parsing of the line is successfull; false otherwise - public static bool CstInfoParser(BlockProcessor blockProcessor, ref StringSlice line, IFencedBlock fenced, char openingCharacter) + public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref StringSlice line, IFencedBlock fenced, char openingCharacter) { string afterFence = null; string info = null; @@ -327,7 +327,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var fencedBlock = block as IFencedBlock; fencedBlock.ClosingFencedCharCount = closingCount; fencedBlock.Newline = processor.Line.Newline; - fencedBlock.WhitespaceBeforeClosingFence = processor.PopBeforeWhitespace(sourcePosition - 1); + fencedBlock.WhitespaceBeforeClosingFence = processor.UseWhitespace(sourcePosition - 1); fencedBlock.AfterWhitespace = new StringSlice(processor.Line.Text, lastFenceCharPosition, line.End); // Don't keep the last line diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index 8daed01e2..b529f69c3 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -31,7 +31,7 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) { IndentCount = processor.Indent, LinesBefore = processor.UseLinesBefore(), - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1), + BeforeWhitespace = processor.UseWhitespace(processor.Start - 1), Newline = processor.Line.Newline, }; } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index bfaa46d90..194a60763 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -86,7 +86,7 @@ public override BlockState TryOpen(BlockProcessor processor) Level = leadingCount, Column = column, Span = { Start = line.Start }, - BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), + BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), Newline = processor.Line.Newline, }; diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index 1bd3007ef..ea60d2ca5 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -41,7 +41,7 @@ public override BlockState TryOpen(BlockProcessor processor) }; var codeBlockLine = new CodeBlockLine { - BeforeWhitespace = processor.PopBeforeWhitespace(sourceStartPosition - 1) + BeforeWhitespace = processor.UseWhitespace(sourceStartPosition - 1) }; codeBlock.CodeBlockLines.Add(codeBlockLine); processor.NewBlocks.Push(codeBlock); @@ -94,7 +94,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var cb = (CodeBlock)block; var codeBlockLine = new CodeBlockLine { - BeforeWhitespace = processor.PopBeforeWhitespace(processor.Start - 1) + BeforeWhitespace = processor.UseWhitespace(processor.Start - 1) }; cb.CodeBlockLines ??= new List(); cb.CodeBlockLines.Add(codeBlockLine); diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 54e29f37d..1232cf0fb 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -210,7 +210,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) } var bulletLength = 1; // TODO: RTP: fix for ordered var savedWhitespaceStart = state.WhitespaceStart; - var whitespaceBefore = state.PopBeforeWhitespace(state.Start - bulletLength - 1); + var whitespaceBefore = state.UseWhitespace(state.Start - bulletLength - 1); state.WhitespaceStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index f8f96ccc1..ce6e88643 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -125,7 +125,7 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Span = new SourceSpan(paragraph.Span.Start, line.Start), Level = level, Lines = paragraph.Lines, - BeforeWhitespace = state.PopBeforeWhitespace(sourcePosition - 1), // remove dashes + BeforeWhitespace = state.UseWhitespace(sourcePosition - 1), // remove dashes AfterWhitespace = new StringSlice(state.Line.Text, state.Start, line.End), LinesBefore = paragraph.LinesBefore, Newline = state.Line.Newline, diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 8cbadd196..1f913a795 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -50,9 +50,9 @@ public override BlockState TryOpen(BlockProcessor processor) processor.Line.TrimStart(); afterWhitespace = new StringSlice(processor.Line.Text, sourcePosition + 1, processor.Line.End); } - quoteBlock.QuoteLines.Add(new QuoteBlock.QuoteLine + quoteBlock.QuoteLines.Add(new QuoteBlockLine { - BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), + BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), AfterWhitespace = afterWhitespace, QuoteChar = true, Newline = processor.Line.Newline, @@ -83,7 +83,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) } else { - quote.QuoteLines.Add(new QuoteBlock.QuoteLine + quote.QuoteLines.Add(new QuoteBlockLine { QuoteChar = false, Newline = processor.Line.Newline, @@ -98,10 +98,10 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) processor.Line.TrimStart(); afterWhitespace = new StringSlice(processor.Line.Text, sourcePosition + 1, processor.Line.End); } - quote.QuoteLines.Add(new QuoteBlock.QuoteLine + quote.QuoteLines.Add(new QuoteBlockLine { QuoteChar = true, - BeforeWhitespace = processor.PopBeforeWhitespace(sourcePosition - 1), + BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), AfterWhitespace = afterWhitespace, Newline = processor.Line.Newline, }); diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index b3c3ba7ea..2ceb54b4e 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -60,7 +60,7 @@ protected Block(BlockParser parser) public StringSlice AfterWhitespace { get; set; } public List LinesBefore { get; set; } - public List LinesAfter { get; internal set; } + public List LinesAfter { get; set; } /// /// Occurs when the process of inlines begin. diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index 9c32eccc7..a7b42b922 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -14,18 +14,6 @@ namespace Markdig.Syntax /// public class QuoteBlock : ContainerBlock { - // ws? >? ws? block|inline - public class QuoteLine - { - public StringSlice BeforeWhitespace { get; set; } - - public StringSlice AfterWhitespace { get; set; } - - public Newline Newline { get; set; } - - // support lazy lines - public bool QuoteChar { get; set; } - } /// /// Initializes a new instance of the class. @@ -35,11 +23,23 @@ public QuoteBlock(BlockParser parser) : base(parser) { } - public List QuoteLines { get; set; } = new List(); + public List QuoteLines { get; set; } = new List(); /// /// Gets or sets the quote character (usually `>`) /// public char QuoteChar { get; set; } } + + public class QuoteBlockLine + { + public StringSlice BeforeWhitespace { get; set; } + + public StringSlice AfterWhitespace { get; set; } + + public Newline Newline { get; set; } + + // support lazy lines + public bool QuoteChar { get; set; } + } } \ No newline at end of file From bebdf0179ecec8b62b2009bcaf0b985f4f459ade Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 20:26:56 +0100 Subject: [PATCH 085/120] respect TrackTrivia flag much more widely, restore intentionally broken behaviors --- src/Markdig.Tests/TestMediaLinks.cs | 30 +++--- src/Markdig/Parsers/BlockProcessor.cs | 7 +- src/Markdig/Parsers/HeadingBlockParser.cs | 27 ++++-- src/Markdig/Parsers/InlineProcessor.cs | 17 ++-- .../Parsers/Inlines/EscapeInlineParser.cs | 51 +++++++--- .../Parsers/Inlines/LineBreakInlineParser.cs | 17 +++- .../Parsers/Inlines/LiteralInlineParser.cs | 21 ++-- src/Markdig/Parsers/ListBlockParser.cs | 95 ++++++++++--------- src/Markdig/Parsers/MarkdownParser.cs | 9 +- src/Markdig/Parsers/OrderedListItemParser.cs | 2 +- src/Markdig/Parsers/ParagraphBlockParser.cs | 10 +- .../Parsers/UnorderedListItemParser.cs | 1 - 12 files changed, 170 insertions(+), 117 deletions(-) diff --git a/src/Markdig.Tests/TestMediaLinks.cs b/src/Markdig.Tests/TestMediaLinks.cs index 27ba34262..bad7034bc 100644 --- a/src/Markdig.Tests/TestMediaLinks.cs +++ b/src/Markdig.Tests/TestMediaLinks.cs @@ -62,21 +62,21 @@ public TestHostProvider(string provider, string replace) } } - [Test] - [TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")] - [TestCase("![p1](//sample.com/video.mp4)", "

\n", @"^//sample.com/(.+)$", @"https://example.com/$1")] - [TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")] - public void TestCustomHostProvider(string markdown, string expected, string provider, string replace) - { - string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions - { - Hosts = - { - new TestHostProvider(provider, replace), - } - })); - Assert.AreEqual(html, expected); - } + //[Test] + //[TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")] + //[TestCase("![p1](//sample.com/video.mp4)", "

\n", @"^//sample.com/(.+)$", @"https://example.com/$1")] + //[TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")] + //public void TestCustomHostProvider(string markdown, string expected, string provider, string replace) + //{ + // string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions + // { + // Hosts = + // { + // new TestHostProvider(provider, replace), + // } + // })); + // Assert.AreEqual(html, expected); + //} [Test] [TestCase("![static mp4](//sample.com/video.mp4)", "

\n", "")] diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 3689610ec..5c116fd1c 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -552,9 +552,12 @@ internal void CloseAll(bool force) { break; } - if (BeforeLines != null && BeforeLines.Count > 0) + if (TrackTrivia) { - block.LinesAfter = UseLinesBefore(); + if (BeforeLines != null && BeforeLines.Count > 0) + { + block.LinesAfter = UseLinesBefore(); + } } Close(i); } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 194a60763..349ccb223 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -73,7 +73,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (leadingCount > 0 && leadingCount <= MaxLeadingCount && (c.IsSpaceOrTab() || c == '\0')) { StringSlice whitespace = StringSlice.Empty; - if (c.IsSpaceOrTab()) + if (processor.TrackTrivia && c.IsSpaceOrTab()) { whitespace = new StringSlice(processor.Line.Text, processor.Start + leadingCount, processor.Start + leadingCount); line.NextChar(); @@ -136,15 +136,19 @@ public override BlockState TryOpen(BlockProcessor processor) // Setup the source end position of this element headingBlock.Span.End = processor.Line.End; - // feed the line to the BlockProcessor without whitespace - processor.Line.Start = headingBlock.Span.Start; - var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); - headingBlock.AfterWhitespace = wsa; - if (wsa.Overlaps(headingBlock.WhitespaceAfterAtxHeaderChar)) + if (processor.TrackTrivia) { - // prevent double whitespace allocation in case of closing # i.e. "# #" - headingBlock.WhitespaceAfterAtxHeaderChar = StringSlice.Empty; + // feed the line to the BlockProcessor without whitespace + processor.Line.Start = headingBlock.Span.Start; + + var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); + headingBlock.AfterWhitespace = wsa; + if (wsa.Overlaps(headingBlock.WhitespaceAfterAtxHeaderChar)) + { + // prevent double whitespace allocation in case of closing # i.e. "# #" + headingBlock.WhitespaceAfterAtxHeaderChar = StringSlice.Empty; + } } // We expect a single line, so don't continue @@ -157,8 +161,11 @@ public override BlockState TryOpen(BlockProcessor processor) public override bool Close(BlockProcessor processor, Block block) { - var heading = (HeadingBlock)block; - //heading.Lines.Trim(); + if (!processor.TrackTrivia) + { + var heading = (HeadingBlock)block; + heading.Lines.Trim(); + } return true; } } diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 4955090dd..54a97f659 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -274,14 +274,17 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) //} } - if (leafBlock is HeadingBlock) + if (TrackTrivia) { - // TODO: RTP: delegate to block? - } - else - { - var newline = leafBlock.Newline; - leafBlock.Inline.AppendChild(new LineBreakInline { Newline = newline }); + if (leafBlock is HeadingBlock) + { + // TODO: RTP: delegate to block? + } + else + { + var newline = leafBlock.Newline; + leafBlock.Inline.AppendChild(new LineBreakInline { Newline = newline }); + } } Inline = null; diff --git a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs index 320499f2d..a9905c207 100644 --- a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs @@ -41,26 +41,47 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } // A backslash at the end of the line is a [hard line break]: - if (c == '\n' || c == '\r') + if (processor.TrackTrivia) { - var newline = c == '\n' ? Newline.LineFeed : Newline.CarriageReturn; - if (c == '\r' && slice.PeekChar() == '\n') + if (c == '\n' || c == '\r') { - newline = Newline.CarriageReturnLineFeed; + var newline = c == '\n' ? Newline.LineFeed : Newline.CarriageReturn; + if (c == '\r' && slice.PeekChar() == '\n') + { + newline = Newline.CarriageReturnLineFeed; + } + processor.Inline = new LineBreakInline() + { + IsHard = true, + IsBackslash = true, + Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, + Line = line, + Column = column, + Newline = newline + }; + processor.Inline.Span.End = processor.Inline.Span.Start + 1; + slice.NextChar(); + return true; } - processor.Inline = new LineBreakInline() + } + else + { + if (c == '\n') { - IsHard = true, - IsBackslash = true, - Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, - Line = line, - Column = column, - Newline = newline - }; - processor.Inline.Span.End = processor.Inline.Span.Start + 1; - slice.NextChar(); - return true; + processor.Inline = new LineBreakInline() + { + IsHard = true, + IsBackslash = true, + Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, + Line = line, + Column = column + }; + processor.Inline.Span.End = processor.Inline.Span.Start + 1; + slice.NextChar(); + return true; + } } + return false; } } diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index 2c713b188..9f640895a 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -38,16 +38,23 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var startPosition = slice.Start; var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace(); var newline = Newline.LineFeed; - if (slice.CurrentChar == '\r') + if (processor.TrackTrivia) { - if (slice.PeekChar() == '\n') + if (slice.CurrentChar == '\r') { - newline = Newline.CarriageReturnLineFeed; - slice.NextChar(); // Skip \n + if (slice.PeekChar() == '\n') + { + newline = Newline.CarriageReturnLineFeed; + slice.NextChar(); // Skip \n + } + else + { + newline = Newline.CarriageReturn; + } } else { - newline = Newline.CarriageReturn; + newline = Newline.LineFeed; } } slice.NextChar(); // Skip \r or \n diff --git a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs index d826d2c70..70f6a725f 100644 --- a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs @@ -50,15 +50,18 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { // Remove line endings if the next char is a new line length = nextStart - slice.Start; - //if (text[nextStart] == '\n') - //{ - // int end = nextStart - 1; - // while (length > 0 && text[end].IsSpace()) - // { - // length--; - // end--; - // } - //} + if (!processor.TrackTrivia) + { + if (text[nextStart] == '\n') + { + int end = nextStart - 1; + while (length > 0 && text[end].IsSpace()) + { + length--; + end--; + } + } + } } // The LiteralInlineParser is always matching (at least an empty string) diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 1232cf0fb..502564d69 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -131,9 +131,10 @@ private BlockState TryContinueListItem(BlockProcessor state, ListItemBlock listI if (!(state.NextContinue is ListBlock)) { list.CountAllBlankLines++; - //listItem.Add(new BlankLineBlock()); - //state.BeforeLines ??= new List(); - //state.BeforeLines.Add(state.Line); + if (!state.TrackTrivia) + { + listItem.Add(new BlankLineBlock()); + } } list.CountBlankLinesReset++; } @@ -320,49 +321,53 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) public override bool Close(BlockProcessor processor, Block blockToClose) { + if (processor.TrackTrivia) + { + return true; + } + + // Process only if we have blank lines + if (blockToClose is ListBlock listBlock && listBlock.CountAllBlankLines > 0) + { + if (listBlock.Parent is ListItemBlock parentListItemBlock && + listBlock.LastChild is ListItemBlock lastListItem && + lastListItem.LastChild is BlankLineBlock) + { + // Inform the outer list that we have a blank line + var parentList = (ListBlock)parentListItemBlock.Parent; + + parentList.CountAllBlankLines++; + parentListItemBlock.Add(new BlankLineBlock()); + } + + for (int listIndex = listBlock.Count - 1; listIndex >= 0; listIndex--) + { + var listItem = (ListItemBlock)listBlock[listIndex]; + + for (int i = listItem.Count - 1; i >= 0; i--) + { + if (listItem[i] is BlankLineBlock) + { + if (i == listItem.Count - 1 ? listIndex < listBlock.Count - 1 : i > 0) + { + listBlock.IsLoose = true; + } + + listItem.RemoveAt(i); + + //If we have removed all blank lines, we can exit + listBlock.CountAllBlankLines--; + if (listBlock.CountAllBlankLines == 0) + { + goto done; + } + } + } + } + } + + done: return true; - // // Process only if we have blank lines - // if (blockToClose is ListBlock listBlock && listBlock.CountAllBlankLines > 0) - // { - // if (listBlock.Parent is ListItemBlock parentListItemBlock && - // listBlock.LastChild is ListItemBlock lastListItem && - // lastListItem.LastChild is BlankLineBlock) - // { - // // Inform the outer list that we have a blank line - // var parentList = (ListBlock)parentListItemBlock.Parent; - - // parentList.CountAllBlankLines++; - // parentListItemBlock.Add(new BlankLineBlock()); - // } - - // for (int listIndex = listBlock.Count - 1; listIndex >= 0; listIndex--) - // { - // var listItem = (ListItemBlock)listBlock[listIndex]; - - // for (int i = listItem.Count - 1; i >= 0; i--) - // { - // if (listItem[i] is BlankLineBlock) - // { - // if (i == listItem.Count - 1 ? listIndex < listBlock.Count - 1 : i > 0) - // { - // listBlock.IsLoose = true; - // } - - // //listItem.RemoveAt(i); - - // // If we have removed all blank lines, we can exit - // //listBlock.CountAllBlankLines--; - // //if (listBlock.CountAllBlankLines == 0) - // //{ - // // goto done; - // //} - // } - // } - // } - // } - - //done: - // return true; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 5714e608c..5c2bc1267 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -117,10 +117,13 @@ private void ProcessBlocks() // If this is the end of file and the last line is empty if (lineText.Text is null) { - var lastBlock = blockProcessor.LastBlock; - if (lastBlock != null && blockProcessor.BeforeLines != null) + if (TrackTrivia) { - lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); + var lastBlock = blockProcessor.LastBlock; + if (lastBlock != null && blockProcessor.BeforeLines != null) + { + lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); + } } break; } diff --git a/src/Markdig/Parsers/OrderedListItemParser.cs b/src/Markdig/Parsers/OrderedListItemParser.cs index 2e20796e1..306129547 100644 --- a/src/Markdig/Parsers/OrderedListItemParser.cs +++ b/src/Markdig/Parsers/OrderedListItemParser.cs @@ -38,7 +38,7 @@ protected bool TryParseDelimiter(BlockProcessor state, out char orderedDelimiter if (delimiter == orderedDelimiter) { state.NextChar(); - state.NextChar(); + state.NextChar(); // TODO: RTP: this is probably a bug return true; } } diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index ce6e88643..d01be5d39 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -73,12 +73,14 @@ public override bool Close(BlockProcessor processor, Block block) return false; } - for (int i = 0; i < lineCount; i++) + if (!processor.TrackTrivia) { - //lines.Lines[i].Slice.TrimStart(); + for (int i = 0; i < lineCount; i++) + { + lines.Lines[i].Slice.TrimStart(); + } + lines.Lines[lineCount - 1].Slice.TrimEnd(); } - - //lines.Lines[lineCount - 1].Slice.TrimEnd(); } return true; diff --git a/src/Markdig/Parsers/UnorderedListItemParser.cs b/src/Markdig/Parsers/UnorderedListItemParser.cs index 4b2d08ef3..5a8e20605 100644 --- a/src/Markdig/Parsers/UnorderedListItemParser.cs +++ b/src/Markdig/Parsers/UnorderedListItemParser.cs @@ -22,7 +22,6 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out { result = new ListInfo(state.CurrentChar); state.NextChar(); - //state.NextChar(); return true; } } From 61b3ffde910a8dd21a22ea22695bcf59cba93633 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 25 Oct 2020 21:11:37 +0100 Subject: [PATCH 086/120] better respect TrackTrivia flag --- src/Markdig.Tests/RoundtripSpecs/Roundtrip.md | 1 + .../Extensions/DefinitionLists/DefinitionListParser.cs | 2 +- src/Markdig/Extensions/Figures/FigureBlockParser.cs | 4 ++-- src/Markdig/Extensions/Tables/PipeTableBlockParser.cs | 2 +- src/Markdig/Syntax/LeafBlock.cs | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md index 364adfe6a..254f0c9dd 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md +++ b/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md @@ -40,6 +40,7 @@ In order: - ~~extract MarkdownRenderer~~ - ~~cleanup NormalizeRenderer (MarkdownRenderer)~~ - ~~deduplicate MarkdownRenderer and NormalizeRenderer code~~ +- use StringSlice where possible instead of String - do pull request feedback - split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? - fix broken pre-existing tests diff --git a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs index 7534f77cb..df7de8ccd 100644 --- a/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs +++ b/src/Markdig/Extensions/DefinitionLists/DefinitionListParser.cs @@ -90,7 +90,7 @@ public override BlockState TryOpen(BlockProcessor processor) Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End), IsOpen = false }; - term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, false); + term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, processor.TrackTrivia); definitionItem.Add(term); } currentDefinitionList.Add(definitionItem); diff --git a/src/Markdig/Extensions/Figures/FigureBlockParser.cs b/src/Markdig/Extensions/Figures/FigureBlockParser.cs index 4f075e800..ca715dcc0 100644 --- a/src/Markdig/Extensions/Figures/FigureBlockParser.cs +++ b/src/Markdig/Extensions/Figures/FigureBlockParser.cs @@ -61,7 +61,7 @@ public override BlockState TryOpen(BlockProcessor processor) Column = column + line.Start - startPosition, IsOpen = false }; - caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, false); + caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, processor.TrackTrivia); figure.Add(caption); } processor.NewBlocks.Push(figure); @@ -96,7 +96,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) Column = column + line.Start - startPosition, IsOpen = false }; - caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, false); + caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition, processor.TrackTrivia); figure.Add(caption); } diff --git a/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs b/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs index 04af5d207..4be262816 100644 --- a/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs +++ b/src/Markdig/Extensions/Tables/PipeTableBlockParser.cs @@ -45,7 +45,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (countPipe > 0) { // Mark the paragraph as open (important, otherwise we would have an infinite loop) - paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start, false); + paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start, processor.TrackTrivia); paragraph.IsOpen = true; return BlockState.BreakDiscard; } diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index fb59d370d..df0e040e3 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -48,7 +48,7 @@ protected LeafBlock(BlockParser parser) : base(parser) /// The column. /// The line. /// - public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition, bool preserveTrivia) + public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition, bool trackTrivia) { if (Lines.Lines == null) { @@ -69,7 +69,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi else { var builder = StringBuilderCache.Local(); - if (preserveTrivia) + if (trackTrivia) { builder.Append(slice.Text, slice.Start, slice.Length); } From 9d83631cfe8bd729dd7f4266432e631bdf409d70 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Mon, 26 Oct 2020 22:37:44 +0100 Subject: [PATCH 087/120] fix horribly broken OrderedList --- .../RoundtripSpecs/TestOrderedList.cs | 12 +++++++ src/Markdig/Parsers/FencedBlockParserBase.cs | 5 ++- src/Markdig/Parsers/ListBlockParser.cs | 5 +-- src/Markdig/Parsers/NumberedListItemParser.cs | 1 - src/Markdig/Parsers/OrderedListItemParser.cs | 1 - .../Renderers/Roundtrip/ListRenderer.cs | 36 +++---------------- 6 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index 0b14fd8b3..4af7dedec 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -168,5 +168,17 @@ public void TestNewline(string value) { RoundTrip(value); } + + [TestCase("1. i\n 1. i")] + [TestCase("1. i\n 1. i\n")] + [TestCase("1. i\n 1. i\n 2. i")] + [TestCase("1. i\n 2. i\n 3. i")] + + [TestCase("1. i\n\t1. i")] + [TestCase("1. i\n\t1. i\n2. i")] + public void TestMultipleLevels(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 5f29c3c65..90665643a 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -269,7 +269,10 @@ public override BlockState TryOpen(BlockProcessor processor) } // specs spaces: Is space and tabs? or only spaces? Use space and tab for this case - //line.TrimStart(); + if (!processor.TrackTrivia) + { + line.TrimStart(); + } var fenced = CreateFencedBlock(processor); { diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 502564d69..2adc6a0de 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -209,9 +209,10 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) state.GoToColumn(initColumn); return BlockState.None; } - var bulletLength = 1; // TODO: RTP: fix for ordered var savedWhitespaceStart = state.WhitespaceStart; - var whitespaceBefore = state.UseWhitespace(state.Start - bulletLength - 1); + var whitespaceBefore = state.UseWhitespace(sourcePosition - 1); + + // set whitespace to the mandatory whitespace after the bullet state.WhitespaceStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; diff --git a/src/Markdig/Parsers/NumberedListItemParser.cs b/src/Markdig/Parsers/NumberedListItemParser.cs index c418b5e6f..9803e64c0 100644 --- a/src/Markdig/Parsers/NumberedListItemParser.cs +++ b/src/Markdig/Parsers/NumberedListItemParser.cs @@ -42,7 +42,6 @@ public override bool TryParse(BlockProcessor state, char pendingBulletType, out { startChar = endChar; } - state.NextChar(); c = state.NextChar(); countDigit++; } diff --git a/src/Markdig/Parsers/OrderedListItemParser.cs b/src/Markdig/Parsers/OrderedListItemParser.cs index 306129547..23d694b83 100644 --- a/src/Markdig/Parsers/OrderedListItemParser.cs +++ b/src/Markdig/Parsers/OrderedListItemParser.cs @@ -38,7 +38,6 @@ protected bool TryParseDelimiter(BlockProcessor state, out char orderedDelimiter if (delimiter == orderedDelimiter) { state.NextChar(); - state.NextChar(); // TODO: RTP: this is probably a bug return true; } } diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index 360866a7d..3e50fbdf5 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -19,46 +19,18 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) renderer.RenderLinesBefore(listBlock); if (listBlock.IsOrdered) { - int index = 0; - if (listBlock.OrderedStart != null) - { - switch (listBlock.BulletType) - { - case '1': - int.TryParse(listBlock.OrderedStart, out index); - break; - } - } - var writeLine = false; for (var i = 0; i < listBlock.Count; i++) { var item = listBlock[i]; var listItem = (ListItemBlock) item; - if (writeLine) - { - renderer.WriteLine(); - } - + renderer.RenderLinesBefore(listItem); renderer.Write(listItem.BeforeWhitespace); - //renderer.Write(index.ToString(CultureInfo.InvariantCulture)); renderer.Write(listItem.SourceBullet); renderer.Write(listBlock.OrderedDelimiter); - renderer.Write(' '); - renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); + //renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); renderer.WriteChildren(listItem); - renderer.PopIndent(); - switch (listBlock.BulletType) - { - case '1': - index++; - break; - } - if (i + 1 < listBlock.Count && listBlock.IsLoose) - { - //renderer.EnsureLine(); - renderer.WriteLine(); - } - writeLine = true; + //renderer.PopIndent(); + renderer.RenderLinesAfter(listItem); } } else From a34e257d2c2cd1f5c0d1b33ba9e7b2d6ec9c42d1 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Mon, 26 Oct 2020 22:59:29 +0100 Subject: [PATCH 088/120] fix ParagraphBlockParser parsing into paragraphs instead of setext headings --- src/Markdig/Parsers/ParagraphBlockParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index d01be5d39..44bb80ab4 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -109,7 +109,7 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) // If we matched a LinkReferenceDefinition before matching the heading, and the remaining // lines are empty, we can early exit and remove the paragraph - if (!foundLrd && paragraph.Lines.Count == 0) + if (!(foundLrd && paragraph.Lines.Count == 0)) { // We discard the paragraph that will be transformed to a heading state.Discard(paragraph); From 2f3e1451b8ce6d1b587be1c6ce316e28fce75959 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Tue, 27 Oct 2020 20:56:57 +0100 Subject: [PATCH 089/120] ensure to write indents when indents are added to the text renderer --- src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs | 7 +++++++ src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs | 1 + src/Markdig/Renderers/TextRendererBase.cs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs index 4af7dedec..8aab2234b 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestOrderedList.cs @@ -180,5 +180,12 @@ public void TestMultipleLevels(string value) { RoundTrip(value); } + + [TestCase("1. c")] + [TestCase("1. c")] + public void Test_IndentedCodeBlock(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs index 3ad24f008..6820db651 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestQuoteBlock.cs @@ -192,6 +192,7 @@ public void Test(string value) [TestCase("> q\n> q")] // 5, 6 [TestCase("> q\n> q")] // 6, 5 [TestCase("> q\n> q")] // 6, 6 + [TestCase("> q\n\n> 5")] // 5, 5 public void TestIndentedCodeBlock(string value) { RoundTrip(value); diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 6196d46b3..9226dc216 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -155,6 +155,10 @@ public void PushIndent(IEnumerable lineSpecific) { if (indents == null) ThrowHelper.ArgumentNullException(nameof(indents)); indents.Add(new Indent(lineSpecific)); + + // ensure that indents are written to the output stream + // this assumes that calls after PushIndent wil write children content + previousWasLine = true; } public void PopIndent() From ef534224f8caf4824273ba9a144167cafcaf121d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 31 Oct 2020 14:34:25 +0100 Subject: [PATCH 090/120] fix heading SoureceSpan bugs --- src/Markdig.Tests/TestNormalize.cs | 1 + src/Markdig/Parsers/FencedBlockParserBase.cs | 1 + src/Markdig/Parsers/FencedCodeBlockParser.cs | 19 ++++++++----------- src/Markdig/Parsers/HeadingBlockParser.cs | 4 ++-- .../Renderers/Roundtrip/ListRenderer.cs | 14 -------------- 5 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/Markdig.Tests/TestNormalize.cs b/src/Markdig.Tests/TestNormalize.cs index 0007f3521..a49c29dc6 100644 --- a/src/Markdig.Tests/TestNormalize.cs +++ b/src/Markdig.Tests/TestNormalize.cs @@ -22,6 +22,7 @@ public void SyntaxCodeBlock() { FencedChar = '`', OpeningFencedCharCount = 4, + ClosingFencedCharCount = 4, Info = "csharp", Lines = new StringLineGroup(4) { diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 90665643a..c2b5e7604 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -84,6 +84,7 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String string afterArg = null; ParseState state = ParseState.AfterFence; + // TODO: RTP: use StringSlices for all // pattern: ``` info? args? // after blockchar? // after info? diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index b529f69c3..9bbc84474 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -39,19 +39,16 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) public override BlockState TryContinue(BlockProcessor processor, Block block) { var result = base.TryContinue(processor, block); - if (result == BlockState.Continue) + if (result == BlockState.Continue && !processor.TrackTrivia) { - if (!processor.TrackTrivia) + var fence = (FencedCodeBlock)block; + // Remove any indent spaces + var c = processor.CurrentChar; + var indentCount = fence.IndentCount; + while (indentCount > 0 && c.IsSpace()) { - var fence = (FencedCodeBlock)block; - // Remove any indent spaces - var c = processor.CurrentChar; - var indentCount = fence.IndentCount; - while (indentCount > 0 && c.IsSpace()) - { - indentCount--; - c = processor.NextChar(); - } + indentCount--; + c = processor.NextChar(); } } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 349ccb223..bd98dcda5 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -85,7 +85,7 @@ public override BlockState TryOpen(BlockProcessor processor) WhitespaceAfterAtxHeaderChar = whitespace, Level = leadingCount, Column = column, - Span = { Start = line.Start }, + Span = { Start = sourcePosition }, BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), Newline = processor.Line.Newline, @@ -140,7 +140,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (processor.TrackTrivia) { // feed the line to the BlockProcessor without whitespace - processor.Line.Start = headingBlock.Span.Start; + //processor.Line.Start = headingBlock.Span.Start; var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); headingBlock.AfterWhitespace = wsa; diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index 3e50fbdf5..a6d274ec5 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -27,9 +27,7 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) renderer.Write(listItem.BeforeWhitespace); renderer.Write(listItem.SourceBullet); renderer.Write(listBlock.OrderedDelimiter); - //renderer.PushIndent(new string(' ', IntLog10Fast(index) + 3)); renderer.WriteChildren(listItem); - //renderer.PopIndent(); renderer.RenderLinesAfter(listItem); } } @@ -62,17 +60,5 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) renderer.RenderLinesAfter(listBlock); } - - - private static int IntLog10Fast(int input) => - (input < 10) ? 0 : - (input < 100) ? 1 : - (input < 1000) ? 2 : - (input < 10000) ? 3 : - (input < 100000) ? 4 : - (input < 1000000) ? 5 : - (input < 10000000) ? 6 : - (input < 100000000) ? 7 : - (input < 1000000000) ? 8 : 9; } } \ No newline at end of file From e3531413e1c58e843706d0db9b61ea15b98d9251 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 31 Oct 2020 15:07:10 +0100 Subject: [PATCH 091/120] fix broken StringSlice tests --- src/Markdig.Tests/TestStringSliceList.cs | 18 ++++++------- src/Markdig/Helpers/StringLine.cs | 1 + src/Markdig/Helpers/StringLineGroup.cs | 32 +++++++++++++++++++----- src/Markdig/Helpers/StringSlice.cs | 12 +++++++++ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/Markdig.Tests/TestStringSliceList.cs b/src/Markdig.Tests/TestStringSliceList.cs index 915e0929f..680efe4bc 100644 --- a/src/Markdig.Tests/TestStringSliceList.cs +++ b/src/Markdig.Tests/TestStringSliceList.cs @@ -16,8 +16,8 @@ public void TestStringLineGroupSimple() { var text = new StringLineGroup(4) { - new StringSlice("ABC"), - new StringSlice("E"), + new StringSlice("ABC", Newline.LineFeed), + new StringSlice("E", Newline.LineFeed), new StringSlice("F") }; @@ -35,8 +35,8 @@ public void TestStringLineGroupWithSlices() { var text = new StringLineGroup(4) { - new StringSlice("XABC") { Start = 1}, - new StringSlice("YYE") { Start = 2}, + new StringSlice("XABC", Newline.LineFeed) { Start = 1}, + new StringSlice("YYE", Newline.LineFeed) { Start = 2}, new StringSlice("ZZZF") { Start = 3 } }; @@ -61,7 +61,7 @@ public void TestStringLineGroupSaveAndRestore() { var text = new StringLineGroup(4) { - new StringSlice("ABCD"), + new StringSlice("ABCD", Newline.LineFeed), new StringSlice("EF"), }.ToCharIterator(); @@ -99,7 +99,7 @@ public void TestSkipWhitespaces() [Test] public void TestStringLineGroupWithModifiedStart() { - var line1 = new StringSlice(" ABC"); + var line1 = new StringSlice(" ABC", Newline.LineFeed); line1.NextChar(); line1.NextChar(); @@ -115,7 +115,7 @@ public void TestStringLineGroupWithModifiedStart() [Test] public void TestStringLineGroupWithTrim() { - var line1 = new StringSlice(" ABC "); + var line1 = new StringSlice(" ABC ", Newline.LineFeed); line1.NextChar(); line1.NextChar(); @@ -133,8 +133,8 @@ public void TestStringLineGroupIteratorPeekChar() { var iterator = new StringLineGroup(4) { - new StringSlice("ABC"), - new StringSlice("E"), + new StringSlice("ABC", Newline.LineFeed), + new StringSlice("E", Newline.LineFeed), new StringSlice("F") }.ToCharIterator(); diff --git a/src/Markdig/Helpers/StringLine.cs b/src/Markdig/Helpers/StringLine.cs index 67c9303be..b90f2b6bb 100644 --- a/src/Markdig/Helpers/StringLine.cs +++ b/src/Markdig/Helpers/StringLine.cs @@ -16,6 +16,7 @@ public struct StringLine public StringLine(ref StringSlice slice) : this() { Slice = slice; + Newline = slice.Newline; } /// diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index e23caf0e8..0abf15f2a 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -277,15 +277,35 @@ public char NextChar() } else { - CurrentChar = slice[slice.Start + _offset]; - if (CurrentChar == '\r' && slice[slice.Start + _offset + 1] == '\n') + var newline = slice.Newline; + if (_offset == slice.Length) { - + if (newline == Newline.LineFeed) + { + CurrentChar = '\n'; + SliceIndex++; + _offset = -1; + } + else if (newline == Newline.CarriageReturn) + { + CurrentChar = '\r'; + SliceIndex++; + _offset = -1; + } + else if (newline == Newline.CarriageReturnLineFeed) + { + CurrentChar = '\r'; + SliceIndex++; + } } - else + else if (_offset + 1 == slice.Length) { - SliceIndex++; - _offset = -1; + if (newline == Newline.CarriageReturnLineFeed) + { + CurrentChar = '\n'; + SliceIndex++; + _offset = -1; + } } } } diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index e77c13c90..80ce5a41a 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -68,6 +68,18 @@ public StringSlice(string text) Newline = Newline.None; } + /// + /// Initializes a new instance of the struct. + /// + /// The text. + public StringSlice(string text, Newline newline) + { + Text = text; + Start = 0; + End = (Text?.Length ?? 0) - 1; + Newline = newline; + } + /// /// Initializes a new instance of the struct. /// From 68d2a20d2065742c10172fe6ddbe1b8445938c9c Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 31 Oct 2020 15:38:00 +0100 Subject: [PATCH 092/120] fix broken PeekChar() of StringLineGroup.Iterator --- src/Markdig/Helpers/StringLineGroup.cs | 38 ++++++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index 0abf15f2a..8f375853c 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -295,7 +295,6 @@ public char NextChar() else if (newline == Newline.CarriageReturnLineFeed) { CurrentChar = '\r'; - SliceIndex++; } } else if (_offset + 1 == slice.Length) @@ -330,20 +329,41 @@ public readonly char PeekChar(int offset = 1) offset += _offset; int sliceIndex = SliceIndex; - var slice = _lines.Lines[sliceIndex].Slice; - - while (offset > slice.Length) + var line = _lines.Lines[sliceIndex]; + var slice = line.Slice; + if (!(line.Newline == Newline.CarriageReturnLineFeed && offset + 1 == slice.Length)) { - // We are not peeking at the same line - offset -= slice.Length + 1; // + 1 for new line + while (offset > slice.Length) + { + // We are not peeking at the same line + offset -= slice.Length + 1; // + 1 for new line - Debug.Assert(sliceIndex + 1 < _lines.Lines.Length, "'Start + offset > End' check above should prevent us from indexing out of range"); - slice = _lines.Lines[++sliceIndex].Slice; + Debug.Assert(sliceIndex + 1 < _lines.Lines.Length, "'Start + offset > End' check above should prevent us from indexing out of range"); + slice = _lines.Lines[++sliceIndex].Slice; + } + } + else + { + if (slice.Newline == Newline.CarriageReturnLineFeed) + { + return '\n'; // /n of /r/n (second character) + } } if (offset == slice.Length) { - return '\n'; // TODO: RTP: fix + if (line.Newline == Newline.LineFeed) + { + return '\n'; + } + if (line.Newline == Newline.CarriageReturn) + { + return '\r'; + } + if (line.Newline == Newline.CarriageReturnLineFeed) + { + return '\r'; // /r of /r/n (first character) + } } Debug.Assert(offset < slice.Length); From 8e17d9c94ea66b745356a800cb7d97be9067d762 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 1 Nov 2020 13:04:44 +0100 Subject: [PATCH 093/120] fix newlines surrounding IndentedCodeBlock --- .../RoundtripSpecs/TestIndentedCodeBlock.cs | 19 +++++++++ .../Parsers/IndentedCodeBlockParser.cs | 40 +++++++++++++------ .../Parsers/Inlines/LineBreakInlineParser.cs | 2 +- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index ca746fd06..c53ee5376 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -62,5 +62,24 @@ public void TestNewline(string value) { RoundTrip(value); } + + [TestCase(" l\n\n l\n")] + [TestCase(" l\n\n\n l\n")] + public void TestNewlinesInBetweenResultInOneCodeBlock(string value) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + var markdownDocument = Markdown.Parse(value, pipeline, trackTrivia: true); + + Assert.AreEqual(1, markdownDocument.Count); + } + + [TestCase(" l\n\np")] + [TestCase(" l\n\n\np")] + [TestCase(" l\n\n\n\np")] + public void TestParagraph(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index ea60d2ca5..97155040d 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -54,10 +54,9 @@ public override BlockState TryOpen(BlockProcessor processor) public override BlockState TryContinue(BlockProcessor processor, Block block) { - bool isLastLine = processor.Line.Start == processor.Line.End + 1; // TODO: RTP: meh. Does this also work for \r\n? if (!processor.IsCodeIndent || processor.IsBlankLine) { - if (block == null || !processor.IsBlankLine || (processor.IsBlankLine && isLastLine)) + if (block == null || !processor.IsBlankLine) { if (block != null) { @@ -65,9 +64,10 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) // add trailing blank lines to blank lines stack of processor for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) { - if (codeBlock.Lines.Lines[i].Slice.IsEmpty) + var line = codeBlock.Lines.Lines[i]; + if (line.Slice.IsEmpty) { - StringLine line = codeBlock.Lines.Lines[i]; + codeBlock.Lines.RemoveAt(i); processor.BeforeLines ??= new List(); processor.BeforeLines.Add(line.Slice); } @@ -107,20 +107,34 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) public override bool Close(BlockProcessor processor, Block block) { var codeBlock = (CodeBlock)block; - if (codeBlock != null) + if (codeBlock == null) { - // Remove any trailing blankline - for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) + return true; + } + + // Remove any trailing blankline + for (int i = codeBlock.Lines.Count - 1; i >= 0; i--) + { + var line = codeBlock.Lines.Lines[i]; + if (line.Slice.IsEmpty) { - if (codeBlock.Lines.Lines[i].Slice.IsEmpty) - { - codeBlock.Lines.RemoveAt(i); - } - else + codeBlock.Lines.RemoveAt(i); + + // if there are newlines after an indented codeblock, we must transform them + // into empty lines after the block. as whitespace is stripped from the Line + // we get that back from the beforeWhitespace on the CodeBlockLine. + if (processor.TrackTrivia) { - break; + var quoteLine = codeBlock.CodeBlockLines[i]; + var emptyLine = new StringSlice(line.Slice.Text, quoteLine.BeforeWhitespace.Start, line.Slice.End, line.Newline); + block.LinesAfter ??= new List(); + block.LinesAfter.Add(emptyLine); } } + else + { + break; + } } return true; } diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index 9f640895a..beb369e39 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -19,7 +19,7 @@ public class LineBreakInlineParser : InlineParser /// public LineBreakInlineParser() { - OpeningCharacters = new[] {'\n', '\r'}; + OpeningCharacters = new[] {'\n', '\r'}; // TODO: RTP: this causes LineBreakInline to be parsed for non-trivia parser } /// From 929ec36c761292e82d2ea964b76d9ea8784637d5 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 1 Nov 2020 13:48:08 +0100 Subject: [PATCH 094/120] fix FencedCodeBlock parsing --- .../RoundtripSpecs/TestFencedCodeBlock.cs | 7 +++++++ src/Markdig/Parsers/FencedBlockParserBase.cs | 13 ++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index b7d8047eb..a384be974 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -35,6 +35,13 @@ public void Test(string value) RoundTrip(value); } + [TestCase("~~~ aa ``` ~~~\nfoo\n~~~")] + [TestCase("~~~ aa ``` ~~~\nfoo\n~~~ ")] + public void TestTilde(string value) + { + RoundTrip(value); + } + [TestCase("```\n c \n```")] [TestCase("```\n c \r```")] [TestCase("```\n c \r\n```")] diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index c2b5e7604..55bb76ad2 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -95,7 +95,7 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String for (int i = line.Start; i <= line.End; i++) { char c = line.Text[i]; - if (c == '`') + if (c == '`' && openingCharacter == '`') { return false; } @@ -160,7 +160,7 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String } } - end: + end: fenced.WhitespaceAfterFencedChar = afterFence; fenced.Info = HtmlHelper.Unescape(info); fenced.UnescapedInfo = info; @@ -324,15 +324,18 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) // If we have a closing fence, close it and discard the current line // The line must contain only fence opening character followed only by whitespaces. - if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace())) // && line.TrimEnd()) TODO: RTP + var startBeforeTrim = line.Start; + var endBeforeTrim = line.End; + var trimmed = line.TrimEnd(); + if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && trimmed) { - block.UpdateSpanEnd(line.Start - 1); + block.UpdateSpanEnd(startBeforeTrim - 1); var fencedBlock = block as IFencedBlock; fencedBlock.ClosingFencedCharCount = closingCount; fencedBlock.Newline = processor.Line.Newline; fencedBlock.WhitespaceBeforeClosingFence = processor.UseWhitespace(sourcePosition - 1); - fencedBlock.AfterWhitespace = new StringSlice(processor.Line.Text, lastFenceCharPosition, line.End); + fencedBlock.AfterWhitespace = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); // Don't keep the last line return BlockState.BreakDiscard; From 5bb2d92180a72606aafe4397e5fac56e67d21381 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 1 Nov 2020 17:38:12 +0100 Subject: [PATCH 095/120] fix quoteblock > orderedlist > quoteblock rendering --- src/Markdig/Renderers/Roundtrip/ListRenderer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index a6d274ec5..213d912e4 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -24,9 +24,11 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) var item = listBlock[i]; var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); - renderer.Write(listItem.BeforeWhitespace); - renderer.Write(listItem.SourceBullet); - renderer.Write(listBlock.OrderedDelimiter); + + var bws = listItem.BeforeWhitespace.ToString(); + var bullet = listItem.SourceBullet.ToString(); + var delimiter = listBlock.OrderedDelimiter; + renderer.PushIndent(new List { $@"{bws}{bullet}{delimiter}" }); renderer.WriteChildren(listItem); renderer.RenderLinesAfter(listItem); } From a6b9c9a41eb8d87975a4f7035992c214e471e3d0 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 1 Nov 2020 17:52:26 +0100 Subject: [PATCH 096/120] don't trip up SpecFileGen test by placing docs in *Spec folder in .Test project --- src/Markdig.Tests/{RoundtripSpecs => }/Roundtrip.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Markdig.Tests/{RoundtripSpecs => }/Roundtrip.md (100%) diff --git a/src/Markdig.Tests/RoundtripSpecs/Roundtrip.md b/src/Markdig.Tests/Roundtrip.md similarity index 100% rename from src/Markdig.Tests/RoundtripSpecs/Roundtrip.md rename to src/Markdig.Tests/Roundtrip.md From ef495da09726adb72226451059dfb696fe23a46d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 1 Nov 2020 20:03:13 +0100 Subject: [PATCH 097/120] fix whitespace after atx heading char --- src/Markdig/Parsers/HeadingBlockParser.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index bd98dcda5..8315e2449 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -65,7 +65,7 @@ public override BlockState TryOpen(BlockProcessor processor) { break; } - c = line.NextChar(); + c = processor.NextChar(); leadingCount++; } @@ -75,8 +75,8 @@ public override BlockState TryOpen(BlockProcessor processor) StringSlice whitespace = StringSlice.Empty; if (processor.TrackTrivia && c.IsSpaceOrTab()) { - whitespace = new StringSlice(processor.Line.Text, processor.Start + leadingCount, processor.Start + leadingCount); - line.NextChar(); + whitespace = new StringSlice(processor.Line.Text, processor.Start, processor.Start); + processor.NextChar(); } // Move to the content var headingBlock = new HeadingBlock(this) @@ -91,7 +91,10 @@ public override BlockState TryOpen(BlockProcessor processor) Newline = processor.Line.Newline, }; processor.NewBlocks.Push(headingBlock); - processor.GoToColumn(column + leadingCount + 1); + if (!processor.TrackTrivia) + { + processor.GoToColumn(column + leadingCount + 1); + } // Gives a chance to parse attributes TryParseAttributes?.Invoke(processor, ref processor.Line, headingBlock); @@ -139,9 +142,6 @@ public override BlockState TryOpen(BlockProcessor processor) if (processor.TrackTrivia) { - // feed the line to the BlockProcessor without whitespace - //processor.Line.Start = headingBlock.Span.Start; - var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); headingBlock.AfterWhitespace = wsa; if (wsa.Overlaps(headingBlock.WhitespaceAfterAtxHeaderChar)) @@ -156,6 +156,8 @@ public override BlockState TryOpen(BlockProcessor processor) } // Else we don't have an header + processor.Line.Start = sourcePosition; + processor.Column = column; return BlockState.None; } From 35b64052d6e29deba67f4ae56c03c6dcc4190523 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 1 Nov 2020 22:48:04 +0100 Subject: [PATCH 098/120] fix whitespaces for QuoteBlock --- src/Markdig/Parsers/BlockProcessor.cs | 8 +++ src/Markdig/Parsers/QuoteBlockParser.cs | 57 ++++++++++++++++--- .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 3 +- src/Markdig/Syntax/LeafBlock.cs | 8 +-- src/Markdig/Syntax/QuoteBlock.cs | 2 + 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 5c116fd1c..b50269b8a 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -66,6 +66,8 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo Open(document); } + public bool SkipFirstUnwindSpace { get; set; } + /// /// Gets the new blocks to push. A is required to push new blocks that it creates to this property. /// @@ -355,6 +357,12 @@ public void UnwindAllIndents() for (; Line.Start > originalLineStart; Line.Start--) { var c = Line.PeekCharAbsolute(Line.Start - 1); + + // don't unwind all the way next to a '>', but one space right of the '>' if there is a space + if (TrackTrivia && SkipFirstUnwindSpace && Line.Start == WhitespaceStart) + { + break; + } if (c == 0) { break; diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 1f913a795..deb0f01b1 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -44,21 +44,41 @@ public override BlockState TryOpen(BlockProcessor processor) Span = new SourceSpan(sourcePosition, processor.Line.End), LinesBefore = processor.UseLinesBefore() }; + + bool hasSpaceAfterQuoteChar = false; + if (c == ' ') + { + processor.NextColumn(); + hasSpaceAfterQuoteChar = true; + processor.SkipFirstUnwindSpace = true; + } + else if (c == '\t') + { + processor.NextColumn(); + } + + var beforeWhitespace = processor.UseWhitespace(sourcePosition - 1); StringSlice afterWhitespace = StringSlice.Empty; + bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) { - processor.Line.TrimStart(); - afterWhitespace = new StringSlice(processor.Line.Text, sourcePosition + 1, processor.Line.End); + processor.WhitespaceStart = processor.Start; + afterWhitespace = processor.UseWhitespace(processor.Line.End); + wasEmptyLine = true; } quoteBlock.QuoteLines.Add(new QuoteBlockLine { - BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), + BeforeWhitespace = beforeWhitespace, AfterWhitespace = afterWhitespace, QuoteChar = true, + HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, Newline = processor.Line.Newline, }); processor.NewBlocks.Push(quoteBlock); - processor.WhitespaceStart = sourcePosition + 1; + if (!wasEmptyLine) + { + processor.WhitespaceStart = processor.Start; + } return BlockState.Continue; } @@ -75,6 +95,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) // 5.1 Block quotes // A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space. var c = processor.CurrentChar; + bool hasSpaceAfterQuoteChar = false; if (c != quote.QuoteChar) { if (processor.IsBlankLine) @@ -91,22 +112,40 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) return BlockState.None; } } - processor.NextChar(); // Skip quote marker char + c = processor.NextChar(); // Skip quote marker char + if (c == ' ') + { + //processor.NextChar(); + processor.NextColumn(); + hasSpaceAfterQuoteChar = true; + processor.SkipFirstUnwindSpace = true; + } + else if (c == '\t') + { + processor.NextColumn(); + } + var beforeWhiteSpace = processor.UseWhitespace(sourcePosition - 1); StringSlice afterWhitespace = StringSlice.Empty; + bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) { - processor.Line.TrimStart(); - afterWhitespace = new StringSlice(processor.Line.Text, sourcePosition + 1, processor.Line.End); + processor.WhitespaceStart = processor.Start; + afterWhitespace = processor.UseWhitespace(processor.Line.End); + wasEmptyLine = true; } quote.QuoteLines.Add(new QuoteBlockLine { QuoteChar = true, - BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), + HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, + BeforeWhitespace = beforeWhiteSpace, AfterWhitespace = afterWhitespace, Newline = processor.Line.Newline, }); - processor.WhitespaceStart = processor.Start; + if (!wasEmptyLine) + { + processor.WhitespaceStart = processor.Start; + } block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; } diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index b2b627906..d7ac7d0dd 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -24,8 +24,9 @@ protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) { var wsb = quoteLine.BeforeWhitespace.ToString(); var quoteChar = quoteLine.QuoteChar ? ">" : ""; + var spaceAfterQuoteChar = quoteLine.HasSpaceAfterQuoteChar ? " " : ""; var wsa = quoteLine.AfterWhitespace.ToString(); - indents.Add(wsb + quoteChar + wsa); + indents.Add(wsb + quoteChar + spaceAfterQuoteChar + wsa); } bool noChildren = false; if (quoteBlock.Count == 0) diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index 21d5280ef..ca8aaa8ff 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -79,13 +79,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi { Lines = new StringLineGroup(4, ProcessInlines); } - int c = column; - if (Lines.Count != 0) - { - // if not first line, preserve whitespace - c = 0; - } - var stringLine = new StringLine(ref slice, line, c, sourceLinePosition, slice.Newline); + var stringLine = new StringLine(ref slice, line, column, sourceLinePosition, slice.Newline); // Regular case, we are not in the middle of a tab if (slice.CurrentChar != '\t' || !CharHelper.IsAcrossTab(column)) { diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index a7b42b922..45279b108 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -41,5 +41,7 @@ public class QuoteBlockLine // support lazy lines public bool QuoteChar { get; set; } + + public bool HasSpaceAfterQuoteChar { get; set; } } } \ No newline at end of file From 8e8b95c3bbbe572a8eb408fd93d4a2f77d7f8488 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 6 Nov 2020 13:27:46 +0100 Subject: [PATCH 099/120] fix broken newline parsing for LineBreakInline and PipeTableParser --- src/Markdig.Tests/RoundtripSpecs/TestHelper.cs | 12 ------------ src/Markdig/Extensions/Tables/PipeTableParser.cs | 6 +++--- src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 src/Markdig.Tests/RoundtripSpecs/TestHelper.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs b/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs deleted file mode 100644 index a2cce15c8..000000000 --- a/src/Markdig.Tests/RoundtripSpecs/TestHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Markdig.Renderers.Normalize; -using Markdig.Syntax; -using NUnit.Framework; -using System.IO; - -namespace Markdig.Tests.RoundtripSpecs -{ - internal static class TestHelper - { - - } -} diff --git a/src/Markdig/Extensions/Tables/PipeTableParser.cs b/src/Markdig/Extensions/Tables/PipeTableParser.cs index 4caebc1d6..3f649f4fb 100644 --- a/src/Markdig/Extensions/Tables/PipeTableParser.cs +++ b/src/Markdig/Extensions/Tables/PipeTableParser.cs @@ -31,7 +31,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions options = null) { this.lineBreakParser = lineBreakParser ?? throw new ArgumentNullException(nameof(lineBreakParser)); - OpeningCharacters = new[] { '|', '\n' }; + OpeningCharacters = new[] { '|', '\n', '\r' }; Options = options ?? new PipeTableOptions(); } @@ -67,7 +67,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // start for a table. Typically, with this, we can have an attributes {...} // starting on the first line of a pipe table, even if the first line // doesn't have a pipe - if (processor.Inline != null && (localLineIndex > 0 || c == '\n')) + if (processor.Inline != null && (localLineIndex > 0 || c == '\n' || c == '\r')) { return false; } @@ -81,7 +81,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) processor.ParserStates[Index] = tableState; } - if (c == '\n') + if (c == '\n' || c == '\r') { if (!isFirstLineEmpty && !tableState.LineHasPipe) { diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index beb369e39..0244862ec 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -19,7 +19,7 @@ public class LineBreakInlineParser : InlineParser /// public LineBreakInlineParser() { - OpeningCharacters = new[] {'\n', '\r'}; // TODO: RTP: this causes LineBreakInline to be parsed for non-trivia parser + OpeningCharacters = new[] { '\n', '\r' }; // TODO: RTP: this causes LineBreakInline to be parsed for non-trivia parser } /// @@ -57,6 +57,13 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) newline = Newline.LineFeed; } } + else + { + if (slice.CurrentChar == '\r' && slice.PeekChar() == '\n') + { + slice.NextChar(); // Skip \n + } + } slice.NextChar(); // Skip \r or \n processor.Inline = new LineBreakInline @@ -67,7 +74,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) Column = column, Newline = newline }; - processor.Inline.Span.End = processor.Inline.Span.Start; + processor.Inline.Span.End = processor.Inline.Span.Start + (newline == Newline.CarriageReturnLineFeed ? 1 : 0); return true; } } From 453e8239d2133c2d0731a5e8e2ae91c8275c71c6 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Fri, 6 Nov 2020 17:33:27 +0100 Subject: [PATCH 100/120] use StringSlice in IFencedBlock --- src/Markdig.Tests/Roundtrip.md | 6 +- .../RoundtripSpecs/TestFencedCodeBlock.cs | 24 +++---- .../TestLinkReferenceDefinition.cs | 1 - .../CustomContainers/CustomContainer.cs | 30 ++++++-- src/Markdig/Parsers/BlockProcessor.cs | 2 +- src/Markdig/Parsers/FencedBlockParserBase.cs | 56 +++++++-------- .../Parsers/Inlines/LineBreakInlineParser.cs | 2 +- .../Renderers/Roundtrip/CodeBlockRenderer.cs | 6 +- src/Markdig/Syntax/FencedCodeBlock.cs | 55 +++++++------- src/Markdig/Syntax/IFencedBlock.cs | 72 ++++++++++++++----- 10 files changed, 149 insertions(+), 105 deletions(-) diff --git a/src/Markdig.Tests/Roundtrip.md b/src/Markdig.Tests/Roundtrip.md index 254f0c9dd..efb1bd4f2 100644 --- a/src/Markdig.Tests/Roundtrip.md +++ b/src/Markdig.Tests/Roundtrip.md @@ -33,17 +33,18 @@ In order: - ~~support link parsing~~ - ~~support AutolinkInline~~ - ~~generate spec examples as tests for roundtrip~~ -- fix `TODO: RTP: ` - ~~check char.IsWhitespace() calls~~ - ~~check char.IsNewline() calls~~ - ~~introduce feature flag~~ - ~~extract MarkdownRenderer~~ - ~~cleanup NormalizeRenderer (MarkdownRenderer)~~ - ~~deduplicate MarkdownRenderer and NormalizeRenderer code~~ +- ~~merge from main~~ +- fix `TODO: RTP: ` - use StringSlice where possible instead of String - do pull request feedback - split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? -- fix broken pre-existing tests +- ~~fix broken pre-existing tests~~ - document newly added syntax properties - support extensions - review complete PR and follow conventions @@ -51,7 +52,6 @@ In order: - create todo list with perf optimization focus points - optimize perf - `\0` -- merge from main - document how trivia are handled generically and specifically - write tree comparison tests? - write tree visualization tool? diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index a384be974..f675956a9 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -86,20 +86,20 @@ public void TestNewline(string value) RoundTrip(value); } - [TestCase("```\ni a\n```")] - [TestCase("```\ni a a2\n```")] - [TestCase("```\ni a a2 a3\n```")] - [TestCase("```\ni a a2 a3 a4\n```")] + [TestCase("```i a\n```")] + [TestCase("```i a a2\n```")] + [TestCase("```i a a2 a3\n```")] + [TestCase("```i a a2 a3 a4\n```")] - [TestCase("```\ni\ta\n```")] - [TestCase("```\ni\ta a2\n```")] - [TestCase("```\ni\ta a2 a3\n```")] - [TestCase("```\ni\ta a2 a3 a4\n```")] + [TestCase("```i\ta\n```")] + [TestCase("```i\ta a2\n```")] + [TestCase("```i\ta a2 a3\n```")] + [TestCase("```i\ta a2 a3 a4\n```")] - [TestCase("```\ni\ta \n```")] - [TestCase("```\ni\ta a2 \n```")] - [TestCase("```\ni\ta a2 a3 \n```")] - [TestCase("```\ni\ta a2 a3 a4 \n```")] + [TestCase("```i\ta \n```")] + [TestCase("```i\ta a2 \n```")] + [TestCase("```i\ta a2 a3 \n```")] + [TestCase("```i\ta a2 a3 a4 \n```")] public void TestInfoArguments(string value) { RoundTrip(value); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs index 1d7a10173..c569764bc 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestLinkReferenceDefinition.cs @@ -3,7 +3,6 @@ namespace Markdig.Tests.RoundtripSpecs { - // TODO: RTP: ".[a] /r" dont seem to be parsed into LRD! [TestFixture] public class TestLinkReferenceDefinition { diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index ca456922c..e252de313 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -23,24 +23,40 @@ public CustomContainer(BlockParser parser) : base(parser) { } + /// public char FencedChar { get; set; } - public string WhitespaceAfterFencedChar { get; set; } + /// + public int OpeningFencedCharCount { get; set; } + + /// + public StringSlice WhitespaceAfterFencedChar { get; set; } + /// public string Info { get; set; } - public string UnescapedInfo { get; set; } - public string WhitespaceAfterInfo { get; set; } + /// + public StringSlice UnescapedInfo { get; set; } + /// + public StringSlice WhitespaceAfterInfo { get; set; } + + /// public string Arguments { get; set; } - public string UnescapedArguments { get; set; } - public string WhitespaceAfterArguments { get; set; } - public int OpeningFencedCharCount { get; set; } + /// + public StringSlice UnescapedArguments { get; set; } - public int ClosingFencedCharCount { get; set; } + /// + public StringSlice WhitespaceAfterArguments { get; set; } + /// public Newline InfoNewline { get; set; } + + /// public StringSlice WhitespaceBeforeClosingFence { get; set; } + + /// + public int ClosingFencedCharCount { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index b50269b8a..b3ac3abf6 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -834,7 +834,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) } if (TrackTrivia) { - // TODO: RTP: delegate this to container parser classes + // TODO: RTP: delegate this to container parser classes? if (paragraph.Parent is QuoteBlock qb) { var afterWhitespace = UseWhitespace(Start - 1); diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 55bb76ad2..798febd88 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -77,24 +77,19 @@ private enum ParseState /// true if parsing of the line is successfull; false otherwise public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref StringSlice line, IFencedBlock fenced, char openingCharacter) { - string afterFence = null; - string info = null; - string afterInfo = null; - string arg = null; - string afterArg = null; + var start = line.Start; + var end = start - 1; + StringSlice afterFence = new StringSlice(line.Text, start, end); + StringSlice info = new StringSlice(line.Text, start, end); + StringSlice afterInfo = new StringSlice(line.Text, start, end); + StringSlice arg = new StringSlice(line.Text, start, end); + StringSlice afterArg = new StringSlice(line.Text, start, end); ParseState state = ParseState.AfterFence; - // TODO: RTP: use StringSlices for all - // pattern: ``` info? args? - // after blockchar? - // after info? - // after args? - // info: between fencedchars and - - // An info string cannot contain any backticks (unless it is a tilde block) for (int i = line.Start; i <= line.End; i++) { char c = line.Text[i]; + // An info string cannot contain any backticks (unless it is a tilde block) if (c == '`' && openingCharacter == '`') { return false; @@ -104,57 +99,60 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String case ParseState.AfterFence: if (c.IsSpaceOrTab()) { - afterFence += c; + afterFence.End += 1; } else { state = ParseState.Info; - info += c; + info.Start = i; + info.End = i; + afterFence.End = i - 1; } break; case ParseState.Info: if (c.IsSpaceOrTab()) { state = ParseState.AfterInfo; - afterInfo += c; + afterInfo.Start = i; + afterInfo.End = i; } else { - info += c; + info.End += 1; } break; case ParseState.AfterInfo: if (c.IsSpaceOrTab()) { - afterInfo += c; + afterInfo.End += 1; } else { - arg += c; + arg.Start = i; + arg.End = i; state = ParseState.Args; } break; case ParseState.Args: - var start = i - 1; - // walk from end, as rest (including spaces except trailing spaces) is args + // walk from end, as rest (except trailing spaces) is args for (int j = line.End; j > start; j--) { - char cc = line[j]; - if (cc.IsSpaceOrTab()) + c = line[j]; + if (c.IsSpaceOrTab()) { - afterArg = cc + afterArg; + afterArg.Start = i; } else { - var length = j - start + 1; - arg = line.Text.Substring(start, length); + arg.End = j; + afterArg.Start = j + 1; + afterArg.End = line.End; goto end; } } goto end; case ParseState.AfterArgs: { - //throw new Exception("should ot reach this code"); return false; } } @@ -162,10 +160,10 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String end: fenced.WhitespaceAfterFencedChar = afterFence; - fenced.Info = HtmlHelper.Unescape(info); + fenced.Info = HtmlHelper.Unescape(info.ToString()); fenced.UnescapedInfo = info; fenced.WhitespaceAfterInfo = afterInfo; - fenced.Arguments = HtmlHelper.Unescape(arg); + fenced.Arguments = HtmlHelper.Unescape(arg.ToString()); fenced.UnescapedArguments = arg; fenced.WhitespaceAfterArguments = afterArg; fenced.InfoNewline = line.Newline; diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index 0244862ec..d7062e5d0 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -19,7 +19,7 @@ public class LineBreakInlineParser : InlineParser /// public LineBreakInlineParser() { - OpeningCharacters = new[] { '\n', '\r' }; // TODO: RTP: this causes LineBreakInline to be parsed for non-trivia parser + OpeningCharacters = new[] { '\n', '\r' }; } /// diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index 40cbfdc42..ad460a5e8 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -24,7 +24,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); renderer.Write(opening); - if (fencedCodeBlock.WhitespaceAfterFencedChar != null) + if (!fencedCodeBlock.WhitespaceAfterFencedChar.IsEmpty) { renderer.Write(fencedCodeBlock.WhitespaceAfterFencedChar); } @@ -32,7 +32,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) { renderer.Write(fencedCodeBlock.UnescapedInfo); } - if (fencedCodeBlock.WhitespaceAfterInfo != null) + if (!fencedCodeBlock.WhitespaceAfterInfo.IsEmpty) { renderer.Write(fencedCodeBlock.WhitespaceAfterInfo); } @@ -40,7 +40,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) { renderer.Write(fencedCodeBlock.UnescapedArguments); } - if (fencedCodeBlock.WhitespaceAfterArguments != null) + if (!fencedCodeBlock.WhitespaceAfterArguments.IsEmpty) { renderer.Write(fencedCodeBlock.WhitespaceAfterArguments); } diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index 65491d67b..b75ab9e46 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -28,49 +28,46 @@ public FencedCodeBlock(BlockParser parser) : base(parser) IsBreakable = false; } + /// + /// Gets or sets the indent count when the fenced code block was indented + /// and we need to remove up to indent count chars spaces from the begining of a line. + /// + public int IndentCount { get; set; } + /// - public string Info { get; set; } - public string UnescapedInfo { get; set; } + public char FencedChar { get; set; } /// - public string WhitespaceAfterInfo { get; set; } + public int OpeningFencedCharCount { get; set; } - /// - /// Gets or sets the arguments after the . - /// May be null. - /// - public string Arguments { get; set; } - public string UnescapedArguments { get; set; } + /// + public StringSlice WhitespaceAfterFencedChar { get; set; } /// - public string WhitespaceAfterArguments { get; set; } + public string Info { get; set; } - /// - /// Gets or sets the fenced character count used to open this fenced code block. - /// - public int OpeningFencedCharCount { get; set; } + /// + public StringSlice UnescapedInfo { get; set; } - /// - /// Gets or sets the fenced character count used to open this fenced code block. - /// - public int ClosingFencedCharCount { get; set; } + /// + public StringSlice WhitespaceAfterInfo { get; set; } - public StringSlice WhitespaceBeforeClosingFence { get; set; } + /// + public string Arguments { get; set; } - /// - /// Gets or sets the fenced character used to open and close this fenced code block. - /// - public char FencedChar { get; set; } + /// + public StringSlice UnescapedArguments { get; set; } /// - public string WhitespaceAfterFencedChar { get; set; } + public StringSlice WhitespaceAfterArguments { get; set; } + /// public Newline InfoNewline { get; set; } - /// - /// Gets or sets the indent count when the fenced code block was indented - /// and we need to remove up to indent count chars spaces from the begining of a line. - /// - public int IndentCount { get; set; } + /// + public StringSlice WhitespaceBeforeClosingFence { get; set; } + + /// + public int ClosingFencedCharCount { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 0245fd3bd..7bdcc90c9 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Helpers; +using Markdig.Parsers; namespace Markdig.Syntax { @@ -11,55 +12,88 @@ namespace Markdig.Syntax /// public interface IFencedBlock : IBlock { + /// + /// Gets or sets the fenced character used to open and close this fenced code block. + /// + char FencedChar { get; set; } + + /// + /// Gets or sets the fenced character count used to open this fenced code block. + /// + public int OpeningFencedCharCount { get; set; } + + /// + /// Gets or sets the whitespace after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice WhitespaceAfterFencedChar { get; set; } + /// /// Gets or sets the language parsed after the first line of /// the fenced code block. May be null. /// string Info { get; set; } - public string UnescapedInfo { get; set; } /// - /// Gets or sets the language parsed after the first line of - /// the fenced code block and includes any whitespace. May be null. + /// Non-escaped exactly as in source markdown. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + StringSlice UnescapedInfo { get; set; } + + /// + /// Gets or sets the whitespace after the . + /// Trivia: only parsed when is enabled, otherwise + /// . /// - string WhitespaceAfterInfo { get; set; } + StringSlice WhitespaceAfterInfo { get; set; } /// /// Gets or sets the arguments after the . /// May be null. /// string Arguments { get; set; } - public string UnescapedArguments { get; set; } /// - /// Gets or sets the arguments after the . - /// Includes any whitespace. May be null. + /// Non-escaped exactly as in source markdown. + /// Trivia: only parsed when is enabled, otherwise + /// . /// - string WhitespaceAfterArguments { get; set; } + StringSlice UnescapedArguments { get; set; } /// - /// Gets or sets the arguments after the . - /// Includes any whitespace. May be null. + /// Gets or sets the whitespace after the . + /// Trivia: only parsed when is enabled, otherwise + /// . /// - string WhitespaceAfterFencedChar { get; set; } + StringSlice WhitespaceAfterArguments { get; set; } /// - /// Gets or sets the fenced character count used to open this fenced code block. + /// Newline of the line with the opening fenced chars. + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public int OpeningFencedCharCount { get; set; } + Newline InfoNewline { get; set; } /// - /// Gets or sets the fenced character count used to open this fenced code block. + /// Whitespace before the closing fenced chars + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public int ClosingFencedCharCount { get; set; } - public StringSlice WhitespaceBeforeClosingFence { get; set; } + StringSlice WhitespaceBeforeClosingFence { get; set; } /// - /// Gets or sets the fenced character used to open and close this fenced code block. + /// Gets or sets the fenced character count used to close this fenced code block. /// - char FencedChar { get; set; } + int ClosingFencedCharCount { get; set; } + /// + /// Newline after the last line, which is always the line containing the closing fence chars. + /// "Inherited" from . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// Newline Newline { get; set; } - Newline InfoNewline { get; set; } } } \ No newline at end of file From b3953dbe2322f11f83f3256f335937ec43c8391e Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 7 Nov 2020 11:47:12 +0100 Subject: [PATCH 101/120] use StringSlice instead of string in InlineLink and LinkReferenceDefinition --- src/Markdig.Tests/Roundtrip.md | 12 +++- src/Markdig/Helpers/LinkHelper.cs | 68 +++++++++---------- .../Parsers/Inlines/LinkInlineParser.cs | 28 +++++--- src/Markdig/Parsers/ParagraphBlockParser.cs | 9 +++ .../Roundtrip/Inlines/LinkInlineRenderer.cs | 2 +- .../LinkReferenceDefinitionRenderer.cs | 2 +- .../Syntax/Inlines/LinkDelimiterInline.cs | 3 +- src/Markdig/Syntax/Inlines/LinkInline.cs | 8 +-- src/Markdig/Syntax/LinkReferenceDefinition.cs | 21 +++--- 9 files changed, 91 insertions(+), 62 deletions(-) diff --git a/src/Markdig.Tests/Roundtrip.md b/src/Markdig.Tests/Roundtrip.md index efb1bd4f2..e39dba442 100644 --- a/src/Markdig.Tests/Roundtrip.md +++ b/src/Markdig.Tests/Roundtrip.md @@ -58,11 +58,21 @@ In order: # Pull request discussion - LinkHelper duplication + - keep current? + - if not, how to deduplicate? - StringSlice vs String + - StringSlice is preferred - amount of tests - should we create even more permutations using `\v`, `\f`? - newlines - Newline struct itself - handling newlines - should newlines be supported? -- Example 207, 209, 291: Special-casing certain edgecases \ No newline at end of file + - LineBreakInline now also parses /r and /r/n: this effectively removes an optimization +- Example 207, 209, 291: Special-casing certain edgecases +- TrackTrivia flag trickling down into 5-6 classes + - InlineProcessor + - BlockProcessor + - MarkdownParser + - Markdown static class + - LinkReferenceDefinitionHelper diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 7b6ec2254..84157d14a 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -418,9 +418,9 @@ public static bool TryParseInlineLink(ref StringSlice text, out string link, out public static bool TryParseInlineLinkWhitespace( ref StringSlice text, out string link, - out string unescapedLink, + out SourceSpan unescapedLink, out string title, - out string unescapedTitle, + out SourceSpan unescapedTitle, out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, @@ -438,9 +438,9 @@ public static bool TryParseInlineLinkWhitespace( bool isValid = false; var c = text.CurrentChar; link = null; - unescapedLink = null; + unescapedLink = SourceSpan.Empty; title = null; - unescapedTitle = null; + unescapedTitle = SourceSpan.Empty; linkSpan = SourceSpan.Empty; titleSpan = SourceSpan.Empty; @@ -458,10 +458,12 @@ public static bool TryParseInlineLinkWhitespace( text.TrimStart(); whitespaceBeforeLink = new SourceSpan(sourcePosition, text.Start - 1); var pos = text.Start; - if (TryParseUrlWhitespace(ref text, out link, out unescapedLink, out urlHasPointyBrackets)) + if (TryParseUrlWhitespace(ref text, out link, out urlHasPointyBrackets)) { linkSpan.Start = pos; linkSpan.End = text.Start - 1; + unescapedLink.Start = pos + (urlHasPointyBrackets ? 1 : 0); + unescapedLink.End = text.Start - 1 - (urlHasPointyBrackets ? 1 : 0); if (linkSpan.End < linkSpan.Start) { linkSpan = SourceSpan.Empty; @@ -486,10 +488,12 @@ public static bool TryParseInlineLinkWhitespace( { isValid = true; } - else if (TryParseTitleWhitespace(ref text, out title, out unescapedTitle, out titleEnclosingCharacter)) + else if (TryParseTitleWhitespace(ref text, out title, out titleEnclosingCharacter)) { titleSpan.Start = pos; titleSpan.End = text.Start - 1; + unescapedTitle.Start = pos + 1; // skip opening character + unescapedTitle.End = text.Start - 1 - 1; // skip closing character if (titleSpan.End < titleSpan.Start) { titleSpan = SourceSpan.Empty; @@ -616,11 +620,10 @@ public static bool TryParseTitle(ref T text, out string title, out char enclo return isValid; } - public static bool TryParseTitleWhitespace(ref T text, out string title, out string unescapedTitle, out char enclosingCharacter) where T : ICharIterator + public static bool TryParseTitleWhitespace(ref T text, out string title, out char enclosingCharacter) where T : ICharIterator { bool isValid = false; var buffer = StringBuilderCache.Local(); - var unescaped = new StringBuilder(); enclosingCharacter = '\0'; // a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or @@ -650,11 +653,9 @@ public static bool TryParseTitleWhitespace(ref T text, out string title, out hasOnlyWhiteSpacesSinceLastLine = -1; } buffer.Append(c); - unescaped.Append(c); if (c == '\r' && text.PeekChar() == '\n') { buffer.Append('\n'); - unescaped.Append(c); } continue; } @@ -669,7 +670,6 @@ public static bool TryParseTitleWhitespace(ref T text, out string title, out if (hasEscape) { buffer.Append(closingQuote); - unescaped.Append(closingQuote); hasEscape = false; continue; } @@ -688,7 +688,6 @@ public static bool TryParseTitleWhitespace(ref T text, out string title, out if (c == '\\') { hasEscape = true; - unescaped.Append('\\'); continue; } @@ -707,12 +706,10 @@ public static bool TryParseTitleWhitespace(ref T text, out string title, out } buffer.Append(c); - unescaped.Append(c); } } title = isValid ? buffer.ToString() : null; - unescapedTitle = isValid ? unescaped.ToString() : null; buffer.Length = 0; return isValid; } @@ -862,7 +859,7 @@ public static bool TryParseUrl(ref T text, out string link, out bool hasPoint return isValid; } - public static bool TryParseUrlWhitespace(ref T text, out string link, out string unescapedLink, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator + public static bool TryParseUrlWhitespace(ref T text, out string link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator { bool isValid = false; hasPointyBrackets = false; @@ -1004,7 +1001,6 @@ public static bool TryParseUrlWhitespace(ref T text, out string link, out str } link = isValid ? buffer.ToString() : null; - unescapedLink = isValid ? unescaped.ToString() : null; buffer.Length = 0; return isValid; } @@ -1177,14 +1173,14 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( ref T text, out SourceSpan whitespaceBeforeLabel, out string label, - out string labelWithWhitespace, + out SourceSpan labelWithWhitespace, out SourceSpan whitespaceBeforeUrl, // can contain newline out string url, - out string unescapedUrl, + out SourceSpan unescapedUrl, out bool urlHasPointyBrackets, out SourceSpan whitespaceBeforeTitle, // can contain newline out string title, // can contain non-consecutive newlines - out string unescapedTitle, + out SourceSpan unescapedTitle, out char titleEnclosingCharacter, out Newline newline, out SourceSpan whitespaceAfterTitle, @@ -1192,12 +1188,13 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( out SourceSpan urlSpan, out SourceSpan titleSpan) where T : ICharIterator { + labelWithWhitespace = SourceSpan.Empty; whitespaceBeforeUrl = SourceSpan.Empty; url = null; - unescapedUrl = null; + unescapedUrl = SourceSpan.Empty; whitespaceBeforeTitle = SourceSpan.Empty; title = null; - unescapedTitle = null; + unescapedTitle = SourceSpan.Empty; newline = Newline.None; urlSpan = SourceSpan.Empty; @@ -1209,10 +1206,12 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( urlHasPointyBrackets = false; titleEnclosingCharacter = '\0'; - if (!TryParseLabelWhitespace(ref text, out label, out labelWithWhitespace, out labelSpan)) + labelWithWhitespace.Start = text.Start + 1; // skip opening [ + if (!TryParseLabelWhitespace(ref text, out label, out labelSpan)) { return false; } + labelWithWhitespace.End = text.Start - 2; // skip closing ] and subsequent : if (text.CurrentChar != ':') { @@ -1228,11 +1227,13 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( urlSpan.Start = text.Start; bool isAngleBracketsUrl = text.CurrentChar == '<'; - if (!TryParseUrlWhitespace(ref text, out url, out unescapedUrl, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + unescapedUrl.Start = text.Start + (isAngleBracketsUrl ? 1 : 0); + if (!TryParseUrlWhitespace(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) { return false; } urlSpan.End = text.Start - 1; + unescapedUrl.End = text.Start - 1 - (isAngleBracketsUrl ? 1 : 0); int whitespaceBeforeTitleStart = text.Start; var saved = text; @@ -1243,9 +1244,11 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( if (c == '\'' || c == '"' || c == '(') { titleSpan.Start = text.Start; - if (TryParseTitleWhitespace(ref text, out title, out unescapedTitle, out titleEnclosingCharacter)) + unescapedTitle.Start = text.Start + 1; // + 1; // skip opening enclosing character + if (TryParseTitleWhitespace(ref text, out title, out titleEnclosingCharacter)) { titleSpan.End = text.Start - 1; + unescapedTitle.End = text.Start - 1 - 1; // skip closing enclosing character // If we have a title, it requires a whitespace after the url if (!hasWhiteSpaces) { @@ -1282,16 +1285,16 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( { text = saved; title = null; - unescapedTitle = null; + unescapedTitle = SourceSpan.Empty; whitespaceAfterTitle = SourceSpan.Empty; return true; } label = null; url = null; - unescapedUrl = null; + unescapedUrl = SourceSpan.Empty; title = null; - unescapedTitle = null; + unescapedTitle = SourceSpan.Empty; return false; } whitespaceAfterTitle = new SourceSpan(whitespaceAfterTitleStart, text.Start - 1); @@ -1334,9 +1337,9 @@ public static bool TryParseLabel(ref T lines, out string label, out SourceSpa return TryParseLabel(ref lines, false, out label, out labelSpan); } - public static bool TryParseLabelWhitespace(ref T lines, out string label, out string labelWithWhitespace, out SourceSpan labelSpan) where T : ICharIterator + public static bool TryParseLabelWhitespace(ref T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator { - return TryParseLabelWhitespace(ref lines, false, out label, out labelWithWhitespace, out labelSpan); + return TryParseLabelWhitespace(ref lines, false, out label, out labelSpan); } public static bool TryParseLabel(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator @@ -1454,10 +1457,9 @@ public static bool TryParseLabel(ref T lines, bool allowEmpty, out string lab return isValid; } - public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out string label, out string labelWithWhitespace, out SourceSpan labelSpan) where T : ICharIterator + public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator { label = null; - labelWithWhitespace = null; char c = lines.CurrentChar; labelSpan = SourceSpan.Empty; if (c != '[') @@ -1465,7 +1467,6 @@ public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out return false; } var buffer = StringBuilderCache.Local(); - var bufferWhitespace = new StringBuilder(); var startLabel = -1; var endLabel = -1; @@ -1523,7 +1524,6 @@ public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out } label = buffer.ToString(); - labelWithWhitespace = bufferWhitespace.ToString(); isValid = true; } } @@ -1541,7 +1541,6 @@ public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out startLabel = lines.Start; } hasEscape = true; - bufferWhitespace.Append(c); } else { @@ -1568,7 +1567,6 @@ public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out hasNonWhiteSpace = true; } } - bufferWhitespace.Append(c); } previousWhitespace = isWhitespace; } diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 32d0acb54..418c00a75 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -42,9 +42,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } } string label; - string labelWithWhitespace = null; - SourceSpan labelSpan = SourceSpan.Empty; - + SourceSpan labelWithWhitespaceSpan = SourceSpan.Empty; switch (c) { case '[': @@ -52,11 +50,14 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // so we try to resolve it here var saved = slice; + SourceSpan labelSpan; // If the label is followed by either a ( or a [, this is not a shortcut if (processor.TrackTrivia) { - if (LinkHelper.TryParseLabelWhitespace(ref slice, out label, out labelWithWhitespace, out labelSpan)) + if (LinkHelper.TryParseLabelWhitespace(ref slice, out label, out labelSpan)) { + labelWithWhitespaceSpan.Start = labelSpan.Start; // skip opening [ + labelWithWhitespaceSpan.End = labelSpan.End; // skip closing ] if (!processor.Document.ContainsLinkReferenceDefinition(label)) { label = null; @@ -77,6 +78,8 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // Else we insert a LinkDelimiter slice.NextChar(); + //labelWithWhitespaceSpan = processor.GetSourcePositionFromLocalSpan(labelWithWhitespaceSpan); + var labelWithWhitespace = new StringSlice(slice.Text, labelWithWhitespaceSpan.Start, labelWithWhitespaceSpan.End); processor.Inline = new LinkDelimiterInline(this) { Type = DelimiterType.Open, @@ -111,8 +114,9 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) private bool ProcessLinkReference( InlineProcessor state, + StringSlice text, string label, - string labelWithWhitespace, + SourceSpan labelWithWhitespaceSpan, bool isShortcut, SourceSpan labelSpan, LinkDelimiterInline parent, @@ -134,6 +138,7 @@ private bool ProcessLinkReference( // Create a default link if the callback was not found if (link == null) { + var labelWithWhitespace = new StringSlice(text.Text, labelWithWhitespaceSpan.Start, labelWithWhitespaceSpan.End); // Inline Link link = new LinkInline() { @@ -234,9 +239,9 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice if (LinkHelper.TryParseInlineLinkWhitespace( ref text, out string url, - out string unescapedUrl, + out SourceSpan unescapedUrlSpan, out string title, - out string unescapedTitle, + out SourceSpan unescapedTitleSpan, out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, @@ -248,6 +253,8 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice var wsBeforeLink = new StringSlice(text.Text, whitespaceBeforeLink.Start, whitespaceBeforeLink.End); var wsAfterLink = new StringSlice(text.Text, whitespaceAfterLink.Start, whitespaceAfterLink.End); var wsAfterTitle = new StringSlice(text.Text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End); + var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End); // Inline Link var link = new LinkInline() { @@ -333,7 +340,7 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice var labelSpan = SourceSpan.Empty; string label = null; - string labelWithWhitespace = null; + SourceSpan labelWithWhitespace = SourceSpan.Empty; bool isLabelSpanLocal = true; bool isShortcut = false; @@ -357,14 +364,15 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice label = openParent.Label; isShortcut = true; } - if (label != null || LinkHelper.TryParseLabelWhitespace(ref text, true, out label, out labelWithWhitespace, out labelSpan)) + if (label != null || LinkHelper.TryParseLabelWhitespace(ref text, true, out label, out labelSpan)) { + labelWithWhitespace = new SourceSpan(labelSpan.Start, labelSpan.End); if (isLabelSpanLocal) { labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); } - if (ProcessLinkReference(inlineState, label, labelWithWhitespace, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) + if (ProcessLinkReference(inlineState, text, label, labelWithWhitespace, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) { // Remove the open parent openParent.Remove(); diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 44bb80ab4..e2185c664 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -223,8 +223,11 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou ref iterator, out LinkReferenceDefinition lrd, out SourceSpan whitespaceBeforeLabel, + out SourceSpan labelWithWhitespace, out SourceSpan whitespaceBeforeUrl, + out SourceSpan unescapedUrl, out SourceSpan whitespaceBeforeTitle, + out SourceSpan unescapedTitle, out SourceSpan whitespaceAfterTitle)) { state.Document.SetLinkReferenceDefinition(lrd.Label, lrd, false); @@ -237,16 +240,22 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou int startPosition = lines.Lines[0].Slice.Start; whitespaceBeforeLabel = whitespaceBeforeLabel.MoveForward(startPosition); + labelWithWhitespace = labelWithWhitespace.MoveForward(startPosition); whitespaceBeforeUrl = whitespaceBeforeUrl.MoveForward(startPosition); + unescapedUrl = unescapedUrl.MoveForward(startPosition); whitespaceBeforeTitle = whitespaceBeforeTitle.MoveForward(startPosition); + unescapedTitle = unescapedTitle.MoveForward(startPosition); whitespaceAfterTitle = whitespaceAfterTitle.MoveForward(startPosition); lrd.Span = lrd.Span.MoveForward(startPosition); lrd.BeforeWhitespace = new StringSlice(text, whitespaceBeforeLabel.Start, whitespaceBeforeLabel.End); lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); + lrd.LabelWithWhitespace = new StringSlice(text, labelWithWhitespace.Start, labelWithWhitespace.End); lrd.WhitespaceBeforeUrl = new StringSlice(text, whitespaceBeforeUrl.Start, whitespaceBeforeUrl.End); lrd.UrlSpan = lrd.UrlSpan.MoveForward(startPosition); + lrd.UnescapedUrl = new StringSlice(text, unescapedUrl.Start, unescapedUrl.End); lrd.WhitespaceBeforeTitle = new StringSlice(text, whitespaceBeforeTitle.Start, whitespaceBeforeTitle.End); lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); + lrd.UnescapedTitle = new StringSlice(text, unescapedTitle.Start, unescapedTitle.End); lrd.AfterWhitespace = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); lrd.LinesBefore = paragraph.LinesBefore; diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs index 62fd46698..7a7cd3696 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs @@ -52,7 +52,7 @@ protected override void Write(RoundtripRenderer renderer, LinkInline link) } renderer.Write(link.WhitespaceAfterUrl); - if (!string.IsNullOrEmpty(link.UnescapedTitle)) + if (!string.IsNullOrEmpty(link.Title)) { var open = link.TitleEnclosingCharacter; var close = link.TitleEnclosingCharacter; diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs index 63c855360..18342007a 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -29,7 +29,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio } renderer.Write(linkDef.WhitespaceBeforeTitle); - if (linkDef.UnescapedTitle != null) + if (linkDef.Title != null) { var open = linkDef.TitleEnclosingCharacter; var close = linkDef.TitleEnclosingCharacter; diff --git a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs index 8ebaf4694..8548ebb68 100644 --- a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Parsers; namespace Markdig.Syntax.Inlines @@ -26,7 +27,7 @@ public LinkDelimiterInline(InlineParser parser) : base(parser) ///
public string Label { get; set; } - public string LabelWithWhitespace { get; set; } + public StringSlice LabelWithWhitespace { get; set; } /// /// The label span diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index 8e67be8d9..96b725df5 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -51,7 +51,7 @@ public LinkInline(string url, string title) /// Gets or sets the URL. /// public string Url { get; set; } - public string UnescapedUrl { get; internal set; } + public StringSlice UnescapedUrl { get; internal set; } public bool UrlHasPointyBrackets { get; set; } @@ -67,7 +67,7 @@ public LinkInline(string url, string title) /// Gets or sets the title. ///
public string Title { get; set; } - public string UnescapedTitle { get; internal set; } + public StringSlice UnescapedTitle { get; internal set; } public char TitleEnclosingCharacter { get; set; } @@ -80,7 +80,7 @@ public LinkInline(string url, string title) public string LinkRefDefLabel { get; set; } - public string LabelWithWhitespace { get; set; } + public StringSlice LabelWithWhitespace { get; set; } /// /// Gets or sets a value indicating whether this instance is an image link. @@ -101,7 +101,7 @@ public LinkInline(string url, string title) /// Gets or sets the reference this link is attached to. May be null. /// public LinkReferenceDefinition Reference { get; set; } - public string LinkRefDefLabelWithWhitespace { get; internal set; } + public StringSlice LinkRefDefLabelWithWhitespace { get; internal set; } public LocalLabel LocalLabel { get; internal set; } /// diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index df48c0e42..71d774e88 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -53,7 +53,7 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// /// Non-normalized Label (includes whitespace) /// - public string LabelWithWhitespace { get; set; } + public StringSlice LabelWithWhitespace { get; set; } public StringSlice WhitespaceBeforeUrl { get; set; } @@ -61,7 +61,7 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// Gets or sets the URL. /// public string Url { get; set; } - public string UnescapedUrl { get; set; } + public StringSlice UnescapedUrl { get; set; } public bool UrlHasPointyBrackets { get; set; } @@ -71,7 +71,7 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// Gets or sets the title. /// public string Title { get; set; } - public string UnescapedTitle { get; set; } + public StringSlice UnescapedTitle { get; set; } public char TitleEnclosingCharacter { get; set; } @@ -138,8 +138,11 @@ public static bool TryParseWhitespace( ref T text, out LinkReferenceDefinition block, out SourceSpan whitespaceBeforeLabel, + out SourceSpan labelWithWhitespace, out SourceSpan whitespaceBeforeUrl, + out SourceSpan unescapedUrl, out SourceSpan whitespaceBeforeTitle, + out SourceSpan unescapedTitle, out SourceSpan whitespaceAfterTitle) where T : ICharIterator { block = null; @@ -150,14 +153,14 @@ public static bool TryParseWhitespace( ref text, out whitespaceBeforeLabel, out string label, - out string labelWithWhitespace, + out labelWithWhitespace, out whitespaceBeforeUrl, out string url, - out string unescapedUrl, + out unescapedUrl, out bool urlHasPointyBrackets, out whitespaceBeforeTitle, out string title, - out string unescapedTitle, + out unescapedTitle, out char titleEnclosingCharacter, out Newline newline, out whitespaceAfterTitle, @@ -172,11 +175,11 @@ public static bool TryParseWhitespace( { UrlHasPointyBrackets = urlHasPointyBrackets, TitleEnclosingCharacter = titleEnclosingCharacter, - LabelWithWhitespace = labelWithWhitespace, + //LabelWithWhitespace = labelWithWhitespace, LabelSpan = labelSpan, UrlSpan = urlSpan, - UnescapedUrl = unescapedUrl, - UnescapedTitle = unescapedTitle, + //UnescapedUrl = unescapedUrl, + //UnescapedTitle = unescapedTitle, TitleSpan = titleSpan, Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End), Newline = newline, From 7dda864e4a2b730010515aa84bc4e3f189b0d007 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 7 Nov 2020 12:36:07 +0100 Subject: [PATCH 102/120] document and cleanup --- src/Markdig/Helpers/StringLine.cs | 3 + src/Markdig/Parsers/BlockProcessor.cs | 42 ++++-- src/Markdig/Parsers/FencedBlockParserBase.cs | 2 +- src/Markdig/Parsers/FencedCodeBlockParser.cs | 2 +- src/Markdig/Parsers/HeadingBlockParser.cs | 4 +- src/Markdig/Parsers/InlineProcessor.cs | 6 +- src/Markdig/Parsers/ListBlockParser.cs | 2 +- src/Markdig/Parsers/ParagraphBlockParser.cs | 8 +- src/Markdig/Parsers/QuoteBlockParser.cs | 12 +- .../Renderers/Roundtrip/CodeBlockRenderer.cs | 4 +- .../Renderers/Roundtrip/HeadingRenderer.cs | 8 +- .../LinkReferenceDefinitionRenderer.cs | 4 +- .../Renderers/Roundtrip/ListRenderer.cs | 6 +- .../Renderers/Roundtrip/ParagraphRenderer.cs | 2 +- .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 4 +- src/Markdig/Syntax/Block.cs | 25 +++- src/Markdig/Syntax/HeadingBlock.cs | 15 ++- src/Markdig/Syntax/IBlock.cs | 4 +- src/Markdig/Syntax/Inlines/CodeInline.cs | 11 ++ .../Syntax/Inlines/LinkDelimiterInline.cs | 9 +- src/Markdig/Syntax/Inlines/LinkInline.cs | 122 +++++++++++++----- src/Markdig/Syntax/LinkReferenceDefinition.cs | 52 ++++++-- src/Markdig/Syntax/ListItemBlock.cs | 5 + src/Markdig/Syntax/QuoteBlock.cs | 48 ++++++- 24 files changed, 302 insertions(+), 98 deletions(-) diff --git a/src/Markdig/Helpers/StringLine.cs b/src/Markdig/Helpers/StringLine.cs index b90f2b6bb..2c37a0a74 100644 --- a/src/Markdig/Helpers/StringLine.cs +++ b/src/Markdig/Helpers/StringLine.cs @@ -71,6 +71,9 @@ public StringLine(ref StringSlice slice, int line, int column, int position, New /// public int Column; + /// + /// The newline. + /// public Newline Newline; /// diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index b3ac3abf6..0a56cda51 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -35,13 +35,6 @@ private BlockProcessor(BlockProcessor root, bool trackTrivia = false) NewBlocks = new Stack(); } - internal List UseLinesBefore() - { - var beforeLines = BeforeLines; - BeforeLines = null; - return beforeLines; - } - /// /// Initializes a new instance of the class. /// @@ -170,25 +163,50 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo private bool ContinueProcessingLine { get; set; } + /// + /// Gets or sets the position of the first character whitespace is encountered + /// and not yet assigned to a syntax node. + /// Trivia: only used when is enabled, otherwise 0. + /// public int WhitespaceStart { get; set; } /// /// Returns trivia that has not yet been assigned to any node and - /// advances the position of trivia to the ending position + /// advances the position of trivia to the ending position. /// /// End position of the trivia /// public StringSlice UseWhitespace(int end) { - // NOTE: Line.Text is full source, not the line (!) var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); WhitespaceStart = end + 1; return stringSlice; } + /// + /// Returns the current stack of to assign it to a . + /// Afterwards, the is set to null. + /// + internal List UseLinesBefore() + { + var beforeLines = BeforeLines; + BeforeLines = null; + return beforeLines; + } + + /// + /// Gets or sets the stack of empty lines not yet assigned to any . + /// An entry may contain an empty . In that case the + /// is relevant. Otherwise, the + /// entry will contain whitespace. + /// public List BeforeLines { get; set; } - public bool TrackTrivia { get; set; } = true; + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; } /// /// Get the current Container that is currently opened @@ -837,8 +855,8 @@ private bool TryOpenBlocks(BlockParser[] parsers) // TODO: RTP: delegate this to container parser classes? if (paragraph.Parent is QuoteBlock qb) { - var afterWhitespace = UseWhitespace(Start - 1); - qb.QuoteLines.Last().AfterWhitespace = afterWhitespace; + var whitespaceAfter = UseWhitespace(Start - 1); + qb.QuoteLines.Last().WhitespaceAfter = whitespaceAfter; } } // We have just found a lazy continuation for a paragraph, early exit diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 798febd88..8b0ded91a 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -333,7 +333,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) fencedBlock.ClosingFencedCharCount = closingCount; fencedBlock.Newline = processor.Line.Newline; fencedBlock.WhitespaceBeforeClosingFence = processor.UseWhitespace(sourcePosition - 1); - fencedBlock.AfterWhitespace = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); + fencedBlock.WhitespaceAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); // Don't keep the last line return BlockState.BreakDiscard; diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index 9bbc84474..1ae774100 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -31,7 +31,7 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) { IndentCount = processor.Indent, LinesBefore = processor.UseLinesBefore(), - BeforeWhitespace = processor.UseWhitespace(processor.Start - 1), + WhitespaceBefore = processor.UseWhitespace(processor.Start - 1), Newline = processor.Line.Newline, }; } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 8315e2449..ec69c808c 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -86,7 +86,7 @@ public override BlockState TryOpen(BlockProcessor processor) Level = leadingCount, Column = column, Span = { Start = sourcePosition }, - BeforeWhitespace = processor.UseWhitespace(sourcePosition - 1), + WhitespaceBefore = processor.UseWhitespace(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), Newline = processor.Line.Newline, }; @@ -143,7 +143,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (processor.TrackTrivia) { var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); - headingBlock.AfterWhitespace = wsa; + headingBlock.WhitespaceAfter = wsa; if (wsa.Overlaps(headingBlock.WhitespaceAfterAtxHeaderChar)) { // prevent double whitespace allocation in case of closing # i.e. "# #" diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 54a97f659..7393dcdc3 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -106,7 +106,11 @@ public InlineProcessor(MarkdownDocument document, InlineParserList parsers, bool /// public TextWriter DebugLog { get; set; } - public bool TrackTrivia { get; } = true; + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; } /// /// Gets the literal inline parser. diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 2adc6a0de..4c4c6859f 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -277,7 +277,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) ColumnWidth = columnWidth, Order = order, SourceBullet = listInfo.SourceBullet, - BeforeWhitespace = whitespaceBefore, + WhitespaceBefore = whitespaceBefore, Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), Newline = state.Line.Newline, diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index e2185c664..cf31e89a3 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -127,8 +127,8 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Span = new SourceSpan(paragraph.Span.Start, line.Start), Level = level, Lines = paragraph.Lines, - BeforeWhitespace = state.UseWhitespace(sourcePosition - 1), // remove dashes - AfterWhitespace = new StringSlice(state.Line.Text, state.Start, line.End), + WhitespaceBefore = state.UseWhitespace(sourcePosition - 1), // remove dashes + WhitespaceAfter = new StringSlice(state.Line.Text, state.Start, line.End), LinesBefore = paragraph.LinesBefore, Newline = state.Line.Newline, IsSetext = true, @@ -247,7 +247,7 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou unescapedTitle = unescapedTitle.MoveForward(startPosition); whitespaceAfterTitle = whitespaceAfterTitle.MoveForward(startPosition); lrd.Span = lrd.Span.MoveForward(startPosition); - lrd.BeforeWhitespace = new StringSlice(text, whitespaceBeforeLabel.Start, whitespaceBeforeLabel.End); + lrd.WhitespaceBefore = new StringSlice(text, whitespaceBeforeLabel.Start, whitespaceBeforeLabel.End); lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); lrd.LabelWithWhitespace = new StringSlice(text, labelWithWhitespace.Start, labelWithWhitespace.End); lrd.WhitespaceBeforeUrl = new StringSlice(text, whitespaceBeforeUrl.Start, whitespaceBeforeUrl.End); @@ -256,7 +256,7 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou lrd.WhitespaceBeforeTitle = new StringSlice(text, whitespaceBeforeTitle.Start, whitespaceBeforeTitle.End); lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); lrd.UnescapedTitle = new StringSlice(text, unescapedTitle.Start, unescapedTitle.End); - lrd.AfterWhitespace = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + lrd.WhitespaceAfter = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); lrd.LinesBefore = paragraph.LinesBefore; state.BeforeLines = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index deb0f01b1..bca54ad59 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -58,18 +58,18 @@ public override BlockState TryOpen(BlockProcessor processor) } var beforeWhitespace = processor.UseWhitespace(sourcePosition - 1); - StringSlice afterWhitespace = StringSlice.Empty; + StringSlice whitespaceAfter = StringSlice.Empty; bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) { processor.WhitespaceStart = processor.Start; - afterWhitespace = processor.UseWhitespace(processor.Line.End); + whitespaceAfter = processor.UseWhitespace(processor.Line.End); wasEmptyLine = true; } quoteBlock.QuoteLines.Add(new QuoteBlockLine { BeforeWhitespace = beforeWhitespace, - AfterWhitespace = afterWhitespace, + WhitespaceAfter = whitespaceAfter, QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, Newline = processor.Line.Newline, @@ -125,12 +125,12 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) processor.NextColumn(); } var beforeWhiteSpace = processor.UseWhitespace(sourcePosition - 1); - StringSlice afterWhitespace = StringSlice.Empty; + StringSlice whitespaceAfter = StringSlice.Empty; bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) { processor.WhitespaceStart = processor.Start; - afterWhitespace = processor.UseWhitespace(processor.Line.End); + whitespaceAfter = processor.UseWhitespace(processor.Line.End); wasEmptyLine = true; } quote.QuoteLines.Add(new QuoteBlockLine @@ -138,7 +138,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, BeforeWhitespace = beforeWhiteSpace, - AfterWhitespace = afterWhitespace, + WhitespaceAfter = whitespaceAfter, Newline = processor.Line.Newline, }); diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index ad460a5e8..df6d43a0d 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -20,7 +20,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) renderer.RenderLinesBefore(obj); if (obj is FencedCodeBlock fencedCodeBlock) { - renderer.Write(obj.BeforeWhitespace); + renderer.Write(obj.WhitespaceBefore); var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); renderer.Write(opening); @@ -65,7 +65,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) // See example 207: "> ```\nfoo\n```" renderer.WriteLine(obj.Newline); } - renderer.Write(obj.AfterWhitespace); + renderer.Write(obj.WhitespaceAfter); } else { diff --git a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs index 55240ba46..2f967d327 100644 --- a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs @@ -32,10 +32,10 @@ protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) renderer.WriteLeafInline(obj); renderer.WriteLine(obj.SetextNewline); - renderer.Write(obj.BeforeWhitespace); + renderer.Write(obj.WhitespaceBefore); renderer.Write(line); renderer.WriteLine(obj.Newline); - renderer.Write(obj.AfterWhitespace); + renderer.Write(obj.WhitespaceAfter); renderer.RenderLinesAfter(obj); } @@ -47,11 +47,11 @@ protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) ? HeadingTexts[obj.Level - 1] : new string('#', obj.Level); - renderer.Write(obj.BeforeWhitespace); + renderer.Write(obj.WhitespaceBefore); renderer.Write(headingText); renderer.Write(obj.WhitespaceAfterAtxHeaderChar); renderer.WriteLeafInline(obj); - renderer.Write(obj.AfterWhitespace); + renderer.Write(obj.WhitespaceAfter); renderer.WriteLine(obj.Newline); renderer.RenderLinesAfter(obj); diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs index 18342007a..091457c60 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -12,7 +12,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio { renderer.RenderLinesBefore(linkDef); - renderer.Write(linkDef.BeforeWhitespace); + renderer.Write(linkDef.WhitespaceBefore); renderer.Write('['); renderer.Write(linkDef.LabelWithWhitespace); renderer.Write("]:"); @@ -41,7 +41,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio renderer.Write(linkDef.UnescapedTitle); renderer.Write(close); } - renderer.Write(linkDef.AfterWhitespace); + renderer.Write(linkDef.WhitespaceAfter); renderer.Write(linkDef.Newline); renderer.RenderLinesAfter(linkDef); diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index 213d912e4..e703dcc1c 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -25,7 +25,7 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); - var bws = listItem.BeforeWhitespace.ToString(); + var bws = listItem.WhitespaceBefore.ToString(); var bullet = listItem.SourceBullet.ToString(); var delimiter = listBlock.OrderedDelimiter; renderer.PushIndent(new List { $@"{bws}{bullet}{delimiter}" }); @@ -41,9 +41,9 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); - StringSlice bws = listItem.BeforeWhitespace; + StringSlice bws = listItem.WhitespaceBefore; char bullet = listBlock.BulletType; - StringSlice aws = listItem.AfterWhitespace; + StringSlice aws = listItem.WhitespaceAfter; renderer.PushIndent(new List { $@"{bws}{bullet}{aws}" }); if (listItem.Count == 0) diff --git a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs index d387d321f..753d2514f 100644 --- a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs @@ -17,7 +17,7 @@ public class ParagraphRenderer : RoundtripObjectRenderer protected override void Write(RoundtripRenderer renderer, ParagraphBlock paragraph) { renderer.RenderLinesBefore(paragraph); - renderer.Write(paragraph.BeforeWhitespace); + renderer.Write(paragraph.WhitespaceBefore); renderer.WriteLeafInline(paragraph); //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes renderer.RenderLinesAfter(paragraph); diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index d7ac7d0dd..a5df64bab 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -17,7 +17,7 @@ public class QuoteBlockRenderer : RoundtripObjectRenderer protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) { renderer.RenderLinesBefore(quoteBlock); - renderer.Write(quoteBlock.BeforeWhitespace); + renderer.Write(quoteBlock.WhitespaceBefore); var indents = new List(); foreach (var quoteLine in quoteBlock.QuoteLines) @@ -25,7 +25,7 @@ protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) var wsb = quoteLine.BeforeWhitespace.ToString(); var quoteChar = quoteLine.QuoteChar ? ">" : ""; var spaceAfterQuoteChar = quoteLine.HasSpaceAfterQuoteChar ? " " : ""; - var wsa = quoteLine.AfterWhitespace.ToString(); + var wsa = quoteLine.WhitespaceAfter.ToString(); indents.Add(wsb + quoteChar + spaceAfterQuoteChar + wsa); } bool noChildren = false; diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index 2ceb54b4e..0ab292778 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -55,11 +55,30 @@ protected Block(BlockParser parser) /// public bool RemoveAfterProcessInlines { get; set; } - // TODO: RTP: rename to WhitespaceBefore - public StringSlice BeforeWhitespace { get; set; } - public StringSlice AfterWhitespace { get; set; } + /// + /// Gets or sets the whitespace right before this block. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice WhitespaceBefore { get; set; } + + /// + /// Gets or sets whitespace occurring after this block. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice WhitespaceAfter { get; set; } + /// + /// Gets or sets the empty lines occurring before this block. + /// Trivia: only parsed when is enabled, otherwise null. + /// public List LinesBefore { get; set; } + + /// + /// Gets or sets the empty lines occurring after this block. + /// Trivia: only parsed when is enabled, otherwise null. + /// public List LinesAfter { get; set; } /// diff --git a/src/Markdig/Syntax/HeadingBlock.cs b/src/Markdig/Syntax/HeadingBlock.cs index 1ddf609e9..eadc76d81 100644 --- a/src/Markdig/Syntax/HeadingBlock.cs +++ b/src/Markdig/Syntax/HeadingBlock.cs @@ -33,13 +33,26 @@ public HeadingBlock(BlockParser parser) : base(parser) /// public int Level { get; set; } + /// + /// True if this heading is a Setext heading. + /// public bool IsSetext { get; set; } + /// + /// Gets or sets the amount of - or = characters when is true. + /// public int HeaderCharCount { get; set; } + /// + /// Gets or sets the newline of the first line when is true. + /// public Newline SetextNewline { get; set; } - // space is mandatory after # only when there is heading content. I.e. "#" is valid + /// + /// Gets or sets the whitespace after the # character when is false. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public StringSlice WhitespaceAfterAtxHeaderChar { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IBlock.cs b/src/Markdig/Syntax/IBlock.cs index 242250f4b..d3d1435a4 100644 --- a/src/Markdig/Syntax/IBlock.cs +++ b/src/Markdig/Syntax/IBlock.cs @@ -58,8 +58,8 @@ public interface IBlock : IMarkdownObject /// event ProcessInlineDelegate ProcessInlinesEnd; - public StringSlice BeforeWhitespace { get; set; } - public StringSlice AfterWhitespace { get; set; } + public StringSlice WhitespaceBefore { get; set; } + public StringSlice WhitespaceAfter { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/CodeInline.cs b/src/Markdig/Syntax/Inlines/CodeInline.cs index 7aa6bff84..e4bff82b3 100644 --- a/src/Markdig/Syntax/Inlines/CodeInline.cs +++ b/src/Markdig/Syntax/Inlines/CodeInline.cs @@ -29,8 +29,19 @@ public class CodeInline : LeafInline /// public string Content { get; set; } + /// + /// Gets or sets the content with trivia and whitespace. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public StringSlice ContentWithTrivia { get; set; } + /// + /// True if the first and last character of the content enclosed in a backtick ` + /// is a space. + /// Trivia: only parsed when is enabled, otherwise + /// false. + /// public bool FirstAndLastWasSpace { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs index 8548ebb68..3503b54ed 100644 --- a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs @@ -27,13 +27,18 @@ public LinkDelimiterInline(InlineParser parser) : base(parser) /// public string Label { get; set; } - public StringSlice LabelWithWhitespace { get; set; } - /// /// The label span /// public SourceSpan LabelSpan; + /// + /// Gets or sets the with whitespace. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice LabelWithWhitespace { get; set; } + public override string ToLiteral() { return IsImage ? "![" : "["; diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index 96b725df5..a1bf62f4d 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -45,69 +45,107 @@ public LinkInline(string url, string title) Title = title; } - public StringSlice WhitespaceBeforeUrl { get; set; } + /// + /// Gets or sets a value indicating whether this instance is an image link. + /// + public bool IsImage { get; set; } /// - /// Gets or sets the URL. + /// Gets or sets the label. /// - public string Url { get; set; } - public StringSlice UnescapedUrl { get; internal set; } + public string Label { get; set; } - public bool UrlHasPointyBrackets { get; set; } + /// + /// The label span + /// + public SourceSpan? LabelSpan; - public StringSlice WhitespaceAfterUrl { get; set; } + /// + /// Gets or sets the with whitespace. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice LabelWithWhitespace { get; set; } /// - /// Gets or sets the GetDynamicUrl delegate. If this property is set, - /// it is used instead of to get the Url from this instance. + /// Gets or sets the type of label parsed + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public GetUrlDelegate GetDynamicUrl { get; set; } + public LocalLabel LocalLabel { get; set; } /// - /// Gets or sets the title. + /// Gets or sets the reference this link is attached to. May be null. /// - public string Title { get; set; } - public StringSlice UnescapedTitle { get; internal set; } + public LinkReferenceDefinition Reference { get; set; } - public char TitleEnclosingCharacter { get; set; } + /// + /// Gets or sets the label as matched against the . + /// + public string LinkRefDefLabel { get; set; } - public StringSlice WhitespaceAfterTitle { get; set; } + /// + /// Gets or sets the with whitespace as matched against + /// the + /// + public StringSlice LinkRefDefLabelWithWhitespace { get; set; } /// - /// Gets or sets the label. + /// Gets or sets the whitespace before the . + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public string Label { get; set; } + public StringSlice WhitespaceBeforeUrl { get; set; } - public string LinkRefDefLabel { get; set; } + /// + /// True if the in the source document is enclosed + /// in pointy brackets. + /// Trivia: only parsed when is enabled, otherwise + /// false. + /// + public bool UrlHasPointyBrackets { get; set; } - public StringSlice LabelWithWhitespace { get; set; } + /// + /// Gets or sets the URL. + /// + public string Url { get; set; } /// - /// Gets or sets a value indicating whether this instance is an image link. + /// The URL source span. /// - public bool IsImage { get; set; } + public SourceSpan? UrlSpan; /// - /// Gets or sets a boolean indicating if this link is a shortcut link to a + /// The but with whitespace and unescaped characters + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public bool IsShortcut { get; set; } + public StringSlice UnescapedUrl { get; set; } /// - /// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized. + /// Any whitespace after the . + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public bool IsAutoLink { get; set; } + public StringSlice WhitespaceAfterUrl { get; set; } /// - /// Gets or sets the reference this link is attached to. May be null. + /// Gets or sets the GetDynamicUrl delegate. If this property is set, + /// it is used instead of to get the Url from this instance. /// - public LinkReferenceDefinition Reference { get; set; } - public StringSlice LinkRefDefLabelWithWhitespace { get; internal set; } - public LocalLabel LocalLabel { get; internal set; } + public GetUrlDelegate GetDynamicUrl { get; set; } /// - /// The URL source span. + /// Gets or sets the character used to enclose the . + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public SourceSpan? UrlSpan; + public char TitleEnclosingCharacter { get; set; } + + /// + /// Gets or sets the title. + /// + public string Title { get; set; } /// /// The title source span. @@ -115,8 +153,28 @@ public LinkInline(string url, string title) public SourceSpan? TitleSpan; /// - /// The label span + /// Gets or sets the exactly as parsed from the + /// source document including unescaped characters + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public SourceSpan? LabelSpan; + public StringSlice UnescapedTitle { get; set; } + + /// + /// Gets or sets the whitespace after the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// + public StringSlice WhitespaceAfterTitle { get; set; } + + /// + /// Gets or sets a boolean indicating if this link is a shortcut link to a + /// + public bool IsShortcut { get; set; } + + /// + /// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized. + /// + public bool IsAutoLink { get; set; } } } diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 71d774e88..baad2f3fa 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -50,46 +50,78 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// public string Label { get; set; } + /// + /// The label span + /// + public SourceSpan LabelSpan; + /// /// Non-normalized Label (includes whitespace) + /// Trivia: only parsed when is enabled, otherwise + /// . /// public StringSlice LabelWithWhitespace { get; set; } + /// + /// Whitespace before the . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public StringSlice WhitespaceBeforeUrl { get; set; } /// /// Gets or sets the URL. /// public string Url { get; set; } + + /// + /// The URL span + /// + public SourceSpan UrlSpan; + + /// + /// Non-normalized . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public StringSlice UnescapedUrl { get; set; } + /// + /// True when the is enclosed in point brackets in the source document. + /// Trivia: only parsed when is enabled, otherwise + /// false. + /// public bool UrlHasPointyBrackets { get; set; } + /// + /// gets or sets the whitespace before a . + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public StringSlice WhitespaceBeforeTitle { get; set; } /// /// Gets or sets the title. /// public string Title { get; set; } - public StringSlice UnescapedTitle { get; set; } - - public char TitleEnclosingCharacter { get; set; } /// - /// The label span + /// The title span /// - public SourceSpan LabelSpan; + public SourceSpan TitleSpan; /// - /// The URL span + /// Non-normalized . + /// Trivia: only parsed when is enabled, otherwise + /// . /// - public SourceSpan UrlSpan; + public StringSlice UnescapedTitle { get; set; } /// - /// The title span + /// Gets or sets the character the is enclosed in. + /// Trivia: only parsed when is enabled, otherwise \0. /// - public SourceSpan TitleSpan; - + public char TitleEnclosingCharacter { get; set; } /// /// Gets or sets the create link inline callback for this instance. diff --git a/src/Markdig/Syntax/ListItemBlock.cs b/src/Markdig/Syntax/ListItemBlock.cs index da36e6fbf..b7956391e 100644 --- a/src/Markdig/Syntax/ListItemBlock.cs +++ b/src/Markdig/Syntax/ListItemBlock.cs @@ -28,6 +28,11 @@ public ListItemBlock(BlockParser parser) : base(parser) /// public int Order { get; set; } + /// + /// Gets or sets the bullet as parsed in the source document. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public StringSlice SourceBullet { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index 45279b108..6dc658570 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -14,7 +14,6 @@ namespace Markdig.Syntax /// public class QuoteBlock : ContainerBlock { - /// /// Initializes a new instance of the class. /// @@ -23,6 +22,11 @@ public QuoteBlock(BlockParser parser) : base(parser) { } + /// + /// Gets or sets the trivia per line of this QuoteBlock. + /// Trivia: only parsed when is enabled, otherwise + /// . + /// public List QuoteLines { get; set; } = new List(); /// @@ -31,17 +35,49 @@ public QuoteBlock(BlockParser parser) : base(parser) public char QuoteChar { get; set; } } + /// + /// Represents trivia per line part of a QuoteBlock. + /// Trivia: only parsed when is enabled, otherwise + /// is empty. + /// public class QuoteBlockLine { + /// + /// Gets or sets whitespace occuring before the first quote character. + /// Trivia: only parsed when is enabled, otherwise + /// is empty. + /// public StringSlice BeforeWhitespace { get; set; } - public StringSlice AfterWhitespace { get; set; } - - public Newline Newline { get; set; } - - // support lazy lines + /// + /// True when this QuoteBlock line has a quote character. False when + /// this line is a "lazy line". + /// Trivia: only parsed when is enabled, otherwise + /// is empty. + /// public bool QuoteChar { get; set; } + /// + /// True if a space is parsed right after the quote character. + /// Trivia: only parsed when is enabled, otherwise + /// is empty. + /// public bool HasSpaceAfterQuoteChar { get; set; } + + /// + /// Gets or sets the whitespace after the the space after the quote character. + /// The first space is assigned to , subsequent + /// whitespace is assigned to this property. + /// Trivia: only parsed when is enabled, otherwise + /// is empty. + /// + public StringSlice WhitespaceAfter { get; set; } + + /// + /// Gets or sets the newline of this QuoeBlockLine. + /// Trivia: only parsed when is enabled, otherwise + /// is empty. + /// + public Newline Newline { get; set; } } } \ No newline at end of file From 9c43b802bdc77e84fcea34d9e01b7cf6b1cabcd3 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 7 Nov 2020 12:38:33 +0100 Subject: [PATCH 103/120] update Roundtrip.md todo list --- src/Markdig.Tests/Roundtrip.md | 10 +++++----- .../RoundtripSpecs/TestFencedCodeBlock.cs | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Markdig.Tests/Roundtrip.md b/src/Markdig.Tests/Roundtrip.md index e39dba442..388447dbe 100644 --- a/src/Markdig.Tests/Roundtrip.md +++ b/src/Markdig.Tests/Roundtrip.md @@ -40,18 +40,18 @@ In order: - ~~cleanup NormalizeRenderer (MarkdownRenderer)~~ - ~~deduplicate MarkdownRenderer and NormalizeRenderer code~~ - ~~merge from main~~ +- ~~fix broken pre-existing tests~~ +- ~~use StringSlice where possible instead of String~~ +- ~~document newly added syntax properties~~ +- ~~review complete PR and follow conventions~~ - fix `TODO: RTP: ` -- use StringSlice where possible instead of String - do pull request feedback -- split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? -- ~~fix broken pre-existing tests~~ -- document newly added syntax properties - support extensions -- review complete PR and follow conventions - run perf test - create todo list with perf optimization focus points - optimize perf - `\0` +- split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? - document how trivia are handled generically and specifically - write tree comparison tests? - write tree visualization tool? diff --git a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs index f675956a9..b49fcc633 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestFencedCodeBlock.cs @@ -3,7 +3,6 @@ namespace Markdig.Tests.RoundtripSpecs { - // TODO: RTP: test info strings [TestFixture] public class TestFencedCodeBlock { From b576b083329b2a91475f428b56d4d36d7601eaaf Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 7 Nov 2020 12:46:22 +0100 Subject: [PATCH 104/120] document MarkdownParser assigning empty lines --- src/Markdig/Parsers/MarkdownParser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 5c2bc1267..a418bb39b 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -120,6 +120,9 @@ private void ProcessBlocks() if (TrackTrivia) { var lastBlock = blockProcessor.LastBlock; + // this means we're out of lines, but still have unassigned empty lines. + // thus, we'll assign the empty unsassigned lines to the last block + // of the document. if (lastBlock != null && blockProcessor.BeforeLines != null) { lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); From 7ab34d5cefcc834ded6b1012cb660d833d11d245 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 7 Nov 2020 18:58:37 +0100 Subject: [PATCH 105/120] fix paragraph with more then 2 newlines after --- src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs | 5 +++++ src/Markdig/Parsers/MarkdownParser.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs index 28855e3a4..27603da98 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestParagraph.cs @@ -198,6 +198,11 @@ public void Test(string value) [TestCase("\r\np\np\r\n")] [TestCase("\r\np\rp\r\n")] [TestCase("\r\np\r\np\r\n")] + + [TestCase("p\n")] + [TestCase("p\n\n")] + [TestCase("p\n\n\n")] + [TestCase("p\n\n\n\n")] public void TestNewline(string value) { RoundTrip(value); diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index a418bb39b..807c18aa0 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -125,7 +125,8 @@ private void ProcessBlocks() // of the document. if (lastBlock != null && blockProcessor.BeforeLines != null) { - lastBlock.LinesAfter = blockProcessor.BeforeLines ?? new List(); + lastBlock.LinesAfter ??= new List(); + lastBlock.LinesAfter.AddRange(blockProcessor.BeforeLines); } } break; From fb4abdfae3763e7b1148b89b4a56395338d15539 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 7 Nov 2020 23:49:25 +0100 Subject: [PATCH 106/120] fix broken multi-empty-line after UnorderedList and make naming of before/after consistent --- .../RoundtripSpecs/TestUnorderedList.cs | 5 +++ src/Markdig/Helpers/LinkHelper.cs | 4 +- src/Markdig/Parsers/BlockProcessor.cs | 39 +++++++++++++------ .../Parsers/IndentedCodeBlockParser.cs | 10 ++--- src/Markdig/Parsers/ListBlockParser.cs | 2 +- src/Markdig/Parsers/MarkdownParser.cs | 8 ++-- src/Markdig/Parsers/ParagraphBlockParser.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 8 ++-- .../Renderers/Roundtrip/CodeBlockRenderer.cs | 2 +- .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 2 +- src/Markdig/Syntax/Block.cs | 9 +++++ src/Markdig/Syntax/CodeBlock.cs | 2 +- src/Markdig/Syntax/QuoteBlock.cs | 2 +- 13 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs index 2247a8491..5a4d17d2b 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestUnorderedList.cs @@ -170,6 +170,11 @@ public void TestFencedCodeBlock(string value) [TestCase("\r\n- i\n- j\r\n")] [TestCase("\r\n- i\r- j\r\n")] [TestCase("\r\n- i\r\n- j\r\n")] + + [TestCase("- i\n")] + [TestCase("- i\n\n")] + [TestCase("- i\n\n\n")] + [TestCase("- i\n\n\n\n")] public void TestNewline(string value) { RoundTrip(value); diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 84157d14a..46037d8d5 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -1219,11 +1219,11 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( return false; } text.NextChar(); // Skip ':' - var beforeWhitespaceUrlStart = text.Start; + var whitespaceBeforeUrlStart = text.Start; // Skip any whitespace before the url text.TrimStart(); - whitespaceBeforeUrl = new SourceSpan(beforeWhitespaceUrlStart, text.Start - 1); + whitespaceBeforeUrl = new SourceSpan(whitespaceBeforeUrlStart, text.Start - 1); urlSpan.Start = text.Start; bool isAngleBracketsUrl = text.CurrentChar == '<'; diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 0a56cda51..3b2243331 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -184,14 +184,14 @@ public StringSlice UseWhitespace(int end) } /// - /// Returns the current stack of to assign it to a . - /// Afterwards, the is set to null. + /// Returns the current stack of to assign it to a . + /// Afterwards, the is set to null. /// internal List UseLinesBefore() { - var beforeLines = BeforeLines; - BeforeLines = null; - return beforeLines; + var linesBefore = LinesBefore; + LinesBefore = null; + return linesBefore; } /// @@ -200,7 +200,7 @@ internal List UseLinesBefore() /// is relevant. Otherwise, the /// entry will contain whitespace. /// - public List BeforeLines { get; set; } + public List LinesBefore { get; set; } /// /// True to parse trivia such as whitespace, extra heading characters and unescaped @@ -580,9 +580,24 @@ internal void CloseAll(bool force) } if (TrackTrivia) { - if (BeforeLines != null && BeforeLines.Count > 0) + if (LinesBefore != null && LinesBefore.Count > 0) { - block.LinesAfter = UseLinesBefore(); + // single emptylines are significant for the syntax tree, attach + // them to the block + if (LinesBefore.Count == 1) + { + block.LinesAfter ??= new List(); + var linesBefore = UseLinesBefore(); + block.LinesAfter.AddRange(linesBefore); + } + else + { + // attach multiple lines after to the root most parent ContainerBlock + var rootMostContainerBlock = Block.FindRootMostContainerParent(block); + rootMostContainerBlock.LinesAfter ??= new List(); + var linesBefore = UseLinesBefore(); + rootMostContainerBlock.LinesAfter.AddRange(linesBefore); + } } } Close(i); @@ -717,9 +732,9 @@ private void TryContinueBlocks() { if (TrackTrivia) { - BeforeLines ??= new List(); + LinesBefore ??= new List(); var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); - BeforeLines.Add(line); + LinesBefore.Add(line); Line.Start = StartBeforeIndent; } } @@ -796,9 +811,9 @@ private bool TryOpenBlocks(BlockParser[] parsers) { if (TrackTrivia) { - BeforeLines ??= new List(); + LinesBefore ??= new List(); var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); - BeforeLines.Add(line); + LinesBefore.Add(line); Line.Start = StartBeforeIndent; } ContinueProcessingLine = false; diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index 97155040d..48ddc91c1 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -41,7 +41,7 @@ public override BlockState TryOpen(BlockProcessor processor) }; var codeBlockLine = new CodeBlockLine { - BeforeWhitespace = processor.UseWhitespace(sourceStartPosition - 1) + WhitespaceBefore = processor.UseWhitespace(sourceStartPosition - 1) }; codeBlock.CodeBlockLines.Add(codeBlockLine); processor.NewBlocks.Push(codeBlock); @@ -68,8 +68,8 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) if (line.Slice.IsEmpty) { codeBlock.Lines.RemoveAt(i); - processor.BeforeLines ??= new List(); - processor.BeforeLines.Add(line.Slice); + processor.LinesBefore ??= new List(); + processor.LinesBefore.Add(line.Slice); } else { @@ -94,7 +94,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var cb = (CodeBlock)block; var codeBlockLine = new CodeBlockLine { - BeforeWhitespace = processor.UseWhitespace(processor.Start - 1) + WhitespaceBefore = processor.UseWhitespace(processor.Start - 1) }; cb.CodeBlockLines ??= new List(); cb.CodeBlockLines.Add(codeBlockLine); @@ -126,7 +126,7 @@ public override bool Close(BlockProcessor processor, Block block) if (processor.TrackTrivia) { var quoteLine = codeBlock.CodeBlockLines[i]; - var emptyLine = new StringSlice(line.Slice.Text, quoteLine.BeforeWhitespace.Start, line.Slice.End, line.Newline); + var emptyLine = new StringSlice(line.Slice.Text, quoteLine.WhitespaceBefore.Start, line.Slice.End, line.Newline); block.LinesAfter ??= new List(); block.LinesAfter.Add(emptyLine); } diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 4c4c6859f..7831e7a2e 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -94,7 +94,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) // TODO: We remove the thematic break, as it will be created later, but this is inefficient, try to find another way var thematicBreak = processor.NewBlocks.Pop(); var linesBefore = thematicBreak.LinesBefore; - processor.BeforeLines = linesBefore; + processor.LinesBefore = linesBefore; return BlockState.None; } } diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 807c18aa0..ea3fdbae4 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -123,10 +123,12 @@ private void ProcessBlocks() // this means we're out of lines, but still have unassigned empty lines. // thus, we'll assign the empty unsassigned lines to the last block // of the document. - if (lastBlock != null && blockProcessor.BeforeLines != null) + if (lastBlock != null && blockProcessor.LinesBefore != null) { - lastBlock.LinesAfter ??= new List(); - lastBlock.LinesAfter.AddRange(blockProcessor.BeforeLines); + var rootMostContainerBlock = Block.FindRootMostContainerParent(lastBlock); + rootMostContainerBlock.LinesAfter ??= new List(); + var linesBefore = blockProcessor.UseLinesBefore(); + rootMostContainerBlock.LinesAfter.AddRange(linesBefore); } } break; diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index cf31e89a3..e8c079e26 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -259,7 +259,7 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou lrd.WhitespaceAfter = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); lrd.LinesBefore = paragraph.LinesBefore; - state.BeforeLines = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack + state.LinesBefore = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack lines = iterator.Remaining(); var index = paragraph.Parent.IndexOf(paragraph); diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index bca54ad59..0769de0bd 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -57,7 +57,7 @@ public override BlockState TryOpen(BlockProcessor processor) processor.NextColumn(); } - var beforeWhitespace = processor.UseWhitespace(sourcePosition - 1); + var whitespaceBefore = processor.UseWhitespace(sourcePosition - 1); StringSlice whitespaceAfter = StringSlice.Empty; bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) @@ -68,7 +68,7 @@ public override BlockState TryOpen(BlockProcessor processor) } quoteBlock.QuoteLines.Add(new QuoteBlockLine { - BeforeWhitespace = beforeWhitespace, + WhitespaceBefore = whitespaceBefore, WhitespaceAfter = whitespaceAfter, QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, @@ -124,7 +124,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { processor.NextColumn(); } - var beforeWhiteSpace = processor.UseWhitespace(sourcePosition - 1); + var whiteSpaceBefore = processor.UseWhitespace(sourcePosition - 1); StringSlice whitespaceAfter = StringSlice.Empty; bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) @@ -137,7 +137,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, - BeforeWhitespace = beforeWhiteSpace, + WhitespaceBefore = whiteSpaceBefore, WhitespaceAfter = whitespaceAfter, Newline = processor.Line.Newline, }); diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index df6d43a0d..823e37755 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -72,7 +72,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) var indents = new List(); foreach (var cbl in obj.CodeBlockLines) { - indents.Add(cbl.BeforeWhitespace.ToString()); + indents.Add(cbl.WhitespaceBefore.ToString()); } renderer.PushIndent(indents); WriteLeafRawLines(renderer, obj); diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index a5df64bab..a4ce46f66 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -22,7 +22,7 @@ protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) var indents = new List(); foreach (var quoteLine in quoteBlock.QuoteLines) { - var wsb = quoteLine.BeforeWhitespace.ToString(); + var wsb = quoteLine.WhitespaceBefore.ToString(); var quoteChar = quoteLine.QuoteChar ? ">" : ""; var spaceAfterQuoteChar = quoteLine.HasSpaceAfterQuoteChar ? " " : ""; var wsa = quoteLine.WhitespaceAfter.ToString(); diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index 0ab292778..1b0fd6cec 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -122,5 +122,14 @@ public void UpdateSpanEnd(int spanEnd) parent = parent.Parent; } } + + internal static Block FindRootMostContainerParent(Block block) + { + if (block.Parent is ContainerBlock && !(block.Parent is MarkdownDocument)) + { + return FindRootMostContainerParent(block.Parent); + } + return block; + } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/CodeBlock.cs b/src/Markdig/Syntax/CodeBlock.cs index 6c22c44d9..2c2296d56 100644 --- a/src/Markdig/Syntax/CodeBlock.cs +++ b/src/Markdig/Syntax/CodeBlock.cs @@ -18,7 +18,7 @@ public class CodeBlock : LeafBlock { public class CodeBlockLine { - public StringSlice BeforeWhitespace { get; set; } + public StringSlice WhitespaceBefore { get; set; } } public List CodeBlockLines { get; set; } = new List(); diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index 6dc658570..a9592e0eb 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -47,7 +47,7 @@ public class QuoteBlockLine /// Trivia: only parsed when is enabled, otherwise /// is empty. /// - public StringSlice BeforeWhitespace { get; set; } + public StringSlice WhitespaceBefore { get; set; } /// /// True when this QuoteBlock line has a quote character. False when From 21b6a3869ae4301b3c671581c9e25f1d3154d138 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 27 Feb 2021 22:00:43 +0100 Subject: [PATCH 107/120] implement NoBlocksFoundBlock, fix null character tests --- .../Inlines/TestNullCharacterInline.cs | 33 ++++++++++++++----- .../RoundtripSpecs/TestNoBlocksFoundBlock.cs | 23 +++++++++++++ src/Markdig/Parsers/MarkdownParser.cs | 19 ++++++++--- .../Roundtrip/NoBlockFoundBlockRenderer.cs | 12 +++++++ .../Renderers/Roundtrip/RoundtripRenderer.cs | 1 + src/Markdig/Syntax/NoBlocksFoundBlock.cs | 15 +++++++++ 6 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs create mode 100644 src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs create mode 100644 src/Markdig/Syntax/NoBlocksFoundBlock.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs index d87eb59b5..4672507ef 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -1,19 +1,36 @@ +using Markdig.Renderers.Roundtrip; +using Markdig.Syntax; using NUnit.Framework; -using static Markdig.Tests.TestRoundtrip; +using System.IO; namespace Markdig.Tests.RoundtripSpecs.Inlines { [TestFixture] public class TestNullCharacterInline { - [TestCase("\0")] - [TestCase("\0p")] - [TestCase("p\0")] - [TestCase("p\0p")] - [TestCase("p\0\0p")] // I promise you, this was not intentional - public void Test(string value) + [TestCase("\0", "")] + [TestCase("\0p", "p")] + [TestCase("p\0", "p")] + [TestCase("p\0p", "pp")] + [TestCase("p\0\0p", "pp")] // I promise you, this was not intentional + public void Test(string value, string expected) { - RoundTrip(value); + RoundTrip(value, expected); + } + + // this method is copied intentionally to ensure all other tests + // do not unintentionally use the expected parameter + private static void RoundTrip(string markdown, string expected) + { + var pipelineBuilder = new MarkdownPipelineBuilder(); + MarkdownPipeline pipeline = pipelineBuilder.Build(); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline, trackTrivia: true); + var sw = new StringWriter(); + var rr = new RoundtripRenderer(sw); + + rr.Write(markdownDocument); + + Assert.AreEqual(expected, sw.ToString()); } } } diff --git a/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs new file mode 100644 index 000000000..806292fb8 --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestNoBlocksFoundBlock.cs @@ -0,0 +1,23 @@ +using NUnit.Framework; +using static Markdig.Tests.TestRoundtrip; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestNoBlocksFoundBlock + { + [TestCase("\r")] + [TestCase("\n")] + [TestCase("\r\n")] + [TestCase("\t")] + [TestCase("\v")] + [TestCase("\f")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(" ")] + public void Test(string value) + { + RoundTrip(value); + } + } +} diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 6c15c00a1..d48e110c0 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -119,12 +119,21 @@ private void ProcessBlocks() { if (TrackTrivia) { - var lastBlock = blockProcessor.LastBlock; - // this means we're out of lines, but still have unassigned empty lines. - // thus, we'll assign the empty unsassigned lines to the last block - // of the document. - if (lastBlock != null && blockProcessor.LinesBefore != null) + Block lastBlock = blockProcessor.LastBlock; + if (lastBlock == null) { + // this means we have unassigned characters + var noBlocksFoundBlock = new NoBlocksFoundBlock(null); + List linesBefore = blockProcessor.UseLinesBefore(); + noBlocksFoundBlock.LinesAfter = new List(); + noBlocksFoundBlock.LinesAfter.AddRange(linesBefore); + document.Add(noBlocksFoundBlock); + } + else if (blockProcessor.LinesBefore != null) + { + // this means we're out of lines, but still have unassigned empty lines. + // thus, we'll assign the empty unsassigned lines to the last block + // of the document. var rootMostContainerBlock = Block.FindRootMostContainerParent(lastBlock); rootMostContainerBlock.LinesAfter ??= new List(); var linesBefore = blockProcessor.UseLinesBefore(); diff --git a/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs new file mode 100644 index 000000000..09c0c7ee4 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs @@ -0,0 +1,12 @@ +using Markdig.Syntax; + +namespace Markdig.Renderers.Roundtrip +{ + public class NoBlockFoundBlockRenderer : RoundtripObjectRenderer + { + protected override void Write(RoundtripRenderer renderer, NoBlocksFoundBlock noBlocksFoundBlock) + { + renderer.RenderLinesAfter(noBlocksFoundBlock); + } + } +} diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs index 12b732966..a2bb5c60e 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -31,6 +31,7 @@ public RoundtripRenderer(TextWriter writer) : base(writer) ObjectRenderers.Add(new ThematicBreakRenderer()); ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); + ObjectRenderers.Add(new NoBlockFoundBlockRenderer()); // Default inline renderers ObjectRenderers.Add(new AutolinkInlineRenderer()); diff --git a/src/Markdig/Syntax/NoBlocksFoundBlock.cs b/src/Markdig/Syntax/NoBlocksFoundBlock.cs new file mode 100644 index 000000000..50528d8c5 --- /dev/null +++ b/src/Markdig/Syntax/NoBlocksFoundBlock.cs @@ -0,0 +1,15 @@ +using Markdig.Parsers; + +namespace Markdig.Syntax +{ + /// + /// Block representing a document with characters but no blocks. This can + /// happen when an input document consists solely of trivia. + /// + public sealed class NoBlocksFoundBlock : LeafBlock + { + public NoBlocksFoundBlock(BlockParser parser) : base(parser) + { + } + } +} From 7035aed74a58975db11632e9c92d904488a9eb9b Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 27 Feb 2021 23:32:09 +0100 Subject: [PATCH 108/120] Refactor Newline struct into enum --- .../Inlines/TestNullCharacterInline.cs | 10 ++--- src/Markdig/Helpers/StringLineGroup.cs | 6 +-- src/Markdig/Helpers/StringSlice.cs | 44 +++++++++++-------- .../LinkReferenceDefinitionRenderer.cs | 3 +- src/Markdig/Renderers/TextRendererBase.cs | 2 +- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs index 4672507ef..ad21b9fa7 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -8,11 +8,11 @@ namespace Markdig.Tests.RoundtripSpecs.Inlines [TestFixture] public class TestNullCharacterInline { - [TestCase("\0", "")] - [TestCase("\0p", "p")] - [TestCase("p\0", "p")] - [TestCase("p\0p", "pp")] - [TestCase("p\0\0p", "pp")] // I promise you, this was not intentional + [TestCase("\0", "\uFFFD")] + [TestCase("\0p", "\uFFFDp")] + [TestCase("p\0", "p\uFFFD")] + [TestCase("p\0p", "p\uFFFDp")] + [TestCase("p\0\0p", "p\uFFFD\uFFFDp")] // I promise you, this was not intentional public void Test(string value, string expected) { RoundTrip(value, expected); diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index 8f375853c..ad795357d 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -145,8 +145,8 @@ public readonly StringSlice ToSlice(List lineOffsets = null) { if (i > 0) { - builder.Append(newline); - previousStartOfLine = builder.Length; + builder.Append(newline.AsString()); + previousStartOfLine = builder.Length; } ref var line = ref Lines[i]; if (!line.Slice.IsEmpty) @@ -225,7 +225,7 @@ public Iterator(StringLineGroup lines) for (int i = 0; i < lines.Count; i++) { var line = lines.Lines[i]; - End += line.Slice.Length + line.Newline.Length; // Add chars + End += line.Slice.Length + line.Newline.Length(); // Add chars } NextChar(); } diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 80ce5a41a..aa1045dbf 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -10,40 +10,46 @@ namespace Markdig.Helpers /// /// Wrap newline so we have type-safety and static accessibility /// - public struct Newline + public enum Newline { - private readonly bool carriageReturn; - private readonly bool lineFeed; - - private Newline(bool carriageReturn, bool lineFeed) - { - this.carriageReturn = carriageReturn; - this.lineFeed = lineFeed; - } - - public static Newline None = new Newline(false, false); - public static Newline CarriageReturn = new Newline(true, false); - public static Newline LineFeed = new Newline(false, true); - public static Newline CarriageReturnLineFeed = new Newline(true, true); + None, + CarriageReturn, + LineFeed, + CarriageReturnLineFeed + } - public static implicit operator string (Newline newline) + public static class NewlineExtensions + { + public static string AsString(this Newline newline) { - if (newline.carriageReturn && newline.lineFeed) + if (newline == Newline.CarriageReturnLineFeed) { return "\r\n"; } - if (newline.lineFeed) + if (newline == Newline.LineFeed) { return "\n"; } - if (newline.carriageReturn) + if (newline == Newline.CarriageReturn) { return "\r"; } return string.Empty; } - public int Length => (carriageReturn ? 1 : 0) + (lineFeed ? 1 : 0); + public static int Length(this Newline newline) => newline switch + { + Newline.None => 0, + Newline.CarriageReturn => 1, + Newline.LineFeed => 1, + Newline.CarriageReturnLineFeed => 2, + _ => throw new NotSupportedException(), + }; } + // public static implicit operator string (Newline newline) + // { + // } + // public int Length => + //} /// /// A lightweight struct that represents a slice of a string. diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs index 091457c60..0520751e9 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using Markdig.Syntax; namespace Markdig.Renderers.Roundtrip @@ -42,7 +43,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio renderer.Write(close); } renderer.Write(linkDef.WhitespaceAfter); - renderer.Write(linkDef.Newline); + renderer.Write(linkDef.Newline.AsString()); renderer.RenderLinesAfter(linkDef); } diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 68e6c6673..c131ac835 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -299,7 +299,7 @@ public T WriteLine() public T WriteLine(Newline newline) { WriteIndent(); - Writer.NewLine = newline; + Writer.NewLine = newline.AsString(); Writer.WriteLine(); previousWasLine = true; return (T)this; From e017adae84ad8bff1e961605995143df0b06305d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sat, 27 Feb 2021 23:38:48 +0100 Subject: [PATCH 109/120] remove TODO: RTPs --- src/Markdig.Tests/Roundtrip.md | 1 - src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs | 2 +- src/Markdig/Parsers/BlockProcessor.cs | 2 +- src/Markdig/Parsers/InlineProcessor.cs | 6 +----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Markdig.Tests/Roundtrip.md b/src/Markdig.Tests/Roundtrip.md index 388447dbe..401e7cc2f 100644 --- a/src/Markdig.Tests/Roundtrip.md +++ b/src/Markdig.Tests/Roundtrip.md @@ -44,7 +44,6 @@ In order: - ~~use StringSlice where possible instead of String~~ - ~~document newly added syntax properties~~ - ~~review complete PR and follow conventions~~ -- fix `TODO: RTP: ` - do pull request feedback - support extensions - run perf test diff --git a/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs b/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs index d68cc3999..b896ebf7d 100644 --- a/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs +++ b/src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs @@ -106,7 +106,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } // Parse URL - if (!LinkHelper.TryParseUrl(ref slice, out string link, out _, true)) // TODO: RTP: store pointy brackets + if (!LinkHelper.TryParseUrl(ref slice, out string link, out _, true)) { return false; } diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 481dc00eb..16c65341b 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -875,7 +875,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) } if (TrackTrivia) { - // TODO: RTP: delegate this to container parser classes? + // special case: take care when refactoring this if (paragraph.Parent is QuoteBlock qb) { var whitespaceAfter = UseWhitespace(Start - 1); diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 18efa74e7..795697e08 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -280,11 +280,7 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) if (TrackTrivia) { - if (leafBlock is HeadingBlock) - { - // TODO: RTP: delegate to block? - } - else + if (!(leafBlock is HeadingBlock)) { var newline = leafBlock.Newline; leafBlock.Inline.AppendChild(new LineBreakInline { Newline = newline }); From 7aeee0681f81bd4e5d631a8b737ba898159c88da Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Mon, 1 Mar 2021 21:12:51 +0100 Subject: [PATCH 110/120] rename Whitespace* member names to Trvia* member names --- .../CustomContainers/CustomContainer.cs | 8 +- src/Markdig/Helpers/LinkHelper.cs | 86 +++++++++---------- src/Markdig/Parsers/BlockProcessor.cs | 24 +++--- src/Markdig/Parsers/FencedBlockParserBase.cs | 10 +-- src/Markdig/Parsers/FencedCodeBlockParser.cs | 2 +- src/Markdig/Parsers/HeadingBlockParser.cs | 14 +-- .../Parsers/IndentedCodeBlockParser.cs | 6 +- .../Parsers/Inlines/LinkInlineParser.cs | 49 ++++++----- src/Markdig/Parsers/ListBlockParser.cs | 14 +-- src/Markdig/Parsers/ParagraphBlockParser.cs | 42 ++++----- src/Markdig/Parsers/QuoteBlockParser.cs | 28 +++--- src/Markdig/Parsers/ThematicBreakParser.cs | 2 +- .../Renderers/Roundtrip/CodeBlockRenderer.cs | 20 ++--- .../Renderers/Roundtrip/HeadingRenderer.cs | 10 +-- .../Roundtrip/Inlines/LinkInlineRenderer.cs | 8 +- .../LinkReferenceDefinitionRenderer.cs | 10 +-- .../Renderers/Roundtrip/ListRenderer.cs | 6 +- .../Renderers/Roundtrip/ParagraphRenderer.cs | 2 +- .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 6 +- src/Markdig/Syntax/Block.cs | 8 +- src/Markdig/Syntax/CodeBlock.cs | 2 +- src/Markdig/Syntax/FencedCodeBlock.cs | 8 +- src/Markdig/Syntax/HeadingBlock.cs | 2 +- src/Markdig/Syntax/IBlock.cs | 4 +- src/Markdig/Syntax/IFencedBlock.cs | 16 ++-- .../Syntax/Inlines/LinkDelimiterInline.cs | 4 +- src/Markdig/Syntax/Inlines/LinkInline.cs | 22 ++--- src/Markdig/Syntax/LinkReferenceDefinition.cs | 32 +++---- src/Markdig/Syntax/QuoteBlock.cs | 10 +-- 29 files changed, 227 insertions(+), 228 deletions(-) diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index e252de313..2a61694e4 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -30,7 +30,7 @@ public CustomContainer(BlockParser parser) : base(parser) public int OpeningFencedCharCount { get; set; } /// - public StringSlice WhitespaceAfterFencedChar { get; set; } + public StringSlice TriviaAfterFencedChar { get; set; } /// public string Info { get; set; } @@ -39,7 +39,7 @@ public CustomContainer(BlockParser parser) : base(parser) public StringSlice UnescapedInfo { get; set; } /// - public StringSlice WhitespaceAfterInfo { get; set; } + public StringSlice TriviaAfterInfo { get; set; } /// public string Arguments { get; set; } @@ -48,13 +48,13 @@ public CustomContainer(BlockParser parser) : base(parser) public StringSlice UnescapedArguments { get; set; } /// - public StringSlice WhitespaceAfterArguments { get; set; } + public StringSlice TriviaAfterArguments { get; set; } /// public Newline InfoNewline { get; set; } /// - public StringSlice WhitespaceBeforeClosingFence { get; set; } + public StringSlice TriviaBeforeClosingFence { get; set; } /// public int ClosingFencedCharCount { get; set; } diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 46037d8d5..fca1901a1 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -415,7 +415,7 @@ public static bool TryParseInlineLink(ref StringSlice text, out string link, out return isValid; } - public static bool TryParseInlineLinkWhitespace( + public static bool TryParseInlineLinkTrivia( ref StringSlice text, out string link, out SourceSpan unescapedLink, @@ -424,9 +424,9 @@ public static bool TryParseInlineLinkWhitespace( out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, - out SourceSpan whitespaceBeforeLink, - out SourceSpan whitespaceAfterLink, - out SourceSpan whitespaceAfterTitle, + out SourceSpan triviaBeforeLink, + out SourceSpan triviaAfterLink, + out SourceSpan triviaAfterTitle, out bool urlHasPointyBrackets) { // 1. An inline link consists of a link text followed immediately by a left parenthesis (, @@ -444,9 +444,9 @@ public static bool TryParseInlineLinkWhitespace( linkSpan = SourceSpan.Empty; titleSpan = SourceSpan.Empty; - whitespaceBeforeLink = SourceSpan.Empty; - whitespaceAfterLink = SourceSpan.Empty; - whitespaceAfterTitle = SourceSpan.Empty; + triviaBeforeLink = SourceSpan.Empty; + triviaAfterLink = SourceSpan.Empty; + triviaAfterTitle = SourceSpan.Empty; urlHasPointyBrackets = false; titleEnclosingCharacter = '\0'; @@ -456,9 +456,9 @@ public static bool TryParseInlineLinkWhitespace( text.NextChar(); var sourcePosition = text.Start; text.TrimStart(); - whitespaceBeforeLink = new SourceSpan(sourcePosition, text.Start - 1); + triviaBeforeLink = new SourceSpan(sourcePosition, text.Start - 1); var pos = text.Start; - if (TryParseUrlWhitespace(ref text, out link, out urlHasPointyBrackets)) + if (TryParseUrlTrivia(ref text, out link, out urlHasPointyBrackets)) { linkSpan.Start = pos; linkSpan.End = text.Start - 1; @@ -469,10 +469,10 @@ public static bool TryParseInlineLinkWhitespace( linkSpan = SourceSpan.Empty; } - int whitespaceStart = text.Start; + int triviaStart = text.Start; text.TrimStart(out int spaceCount); - whitespaceAfterLink = new SourceSpan(whitespaceStart, text.Start - 1); + triviaAfterLink = new SourceSpan(triviaStart, text.Start - 1); var hasWhiteSpaces = spaceCount > 0; c = text.CurrentChar; @@ -488,7 +488,7 @@ public static bool TryParseInlineLinkWhitespace( { isValid = true; } - else if (TryParseTitleWhitespace(ref text, out title, out titleEnclosingCharacter)) + else if (TryParseTitleTrivia(ref text, out title, out titleEnclosingCharacter)) { titleSpan.Start = pos; titleSpan.End = text.Start - 1; @@ -498,9 +498,9 @@ public static bool TryParseInlineLinkWhitespace( { titleSpan = SourceSpan.Empty; } - var startWhitespace = text.Start; + var startTrivia = text.Start; text.TrimStart(); - whitespaceAfterTitle = new SourceSpan(startWhitespace, text.Start - 1); + triviaAfterTitle = new SourceSpan(startTrivia, text.Start - 1); c = text.CurrentChar; if (c == ')') @@ -620,7 +620,7 @@ public static bool TryParseTitle(ref T text, out string title, out char enclo return isValid; } - public static bool TryParseTitleWhitespace(ref T text, out string title, out char enclosingCharacter) where T : ICharIterator + public static bool TryParseTitleTrivia(ref T text, out string title, out char enclosingCharacter) where T : ICharIterator { bool isValid = false; var buffer = StringBuilderCache.Local(); @@ -859,7 +859,7 @@ public static bool TryParseUrl(ref T text, out string link, out bool hasPoint return isValid; } - public static bool TryParseUrlWhitespace(ref T text, out string link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator + public static bool TryParseUrlTrivia(ref T text, out string link, out bool hasPointyBrackets, bool isAutoLink = false) where T : ICharIterator { bool isValid = false; hasPointyBrackets = false; @@ -1169,30 +1169,30 @@ public static bool TryParseLinkReferenceDefinition(ref T text, out string lab return true; } - public static bool TryParseLinkReferenceDefinitionWhitespace( + public static bool TryParseLinkReferenceDefinitionTrivia( ref T text, - out SourceSpan whitespaceBeforeLabel, + out SourceSpan triviaBeforeLabel, out string label, - out SourceSpan labelWithWhitespace, - out SourceSpan whitespaceBeforeUrl, // can contain newline + out SourceSpan labelWithTrivia, + out SourceSpan triviaBeforeUrl, // can contain newline out string url, out SourceSpan unescapedUrl, out bool urlHasPointyBrackets, - out SourceSpan whitespaceBeforeTitle, // can contain newline + out SourceSpan triviaBeforeTitle, // can contain newline out string title, // can contain non-consecutive newlines out SourceSpan unescapedTitle, out char titleEnclosingCharacter, out Newline newline, - out SourceSpan whitespaceAfterTitle, + out SourceSpan triviaAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan) where T : ICharIterator { - labelWithWhitespace = SourceSpan.Empty; - whitespaceBeforeUrl = SourceSpan.Empty; + labelWithTrivia = SourceSpan.Empty; + triviaBeforeUrl = SourceSpan.Empty; url = null; unescapedUrl = SourceSpan.Empty; - whitespaceBeforeTitle = SourceSpan.Empty; + triviaBeforeTitle = SourceSpan.Empty; title = null; unescapedTitle = SourceSpan.Empty; newline = Newline.None; @@ -1201,17 +1201,17 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( titleSpan = SourceSpan.Empty; text.TrimStart(); - whitespaceBeforeLabel = new SourceSpan(0, text.Start - 1); - whitespaceAfterTitle = SourceSpan.Empty; + triviaBeforeLabel = new SourceSpan(0, text.Start - 1); + triviaAfterTitle = SourceSpan.Empty; urlHasPointyBrackets = false; titleEnclosingCharacter = '\0'; - labelWithWhitespace.Start = text.Start + 1; // skip opening [ - if (!TryParseLabelWhitespace(ref text, out label, out labelSpan)) + labelWithTrivia.Start = text.Start + 1; // skip opening [ + if (!TryParseLabelTrivia(ref text, out label, out labelSpan)) { return false; } - labelWithWhitespace.End = text.Start - 2; // skip closing ] and subsequent : + labelWithTrivia.End = text.Start - 2; // skip closing ] and subsequent : if (text.CurrentChar != ':') { @@ -1219,33 +1219,33 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( return false; } text.NextChar(); // Skip ':' - var whitespaceBeforeUrlStart = text.Start; + var triviaBeforeUrlStart = text.Start; // Skip any whitespace before the url text.TrimStart(); - whitespaceBeforeUrl = new SourceSpan(whitespaceBeforeUrlStart, text.Start - 1); + triviaBeforeUrl = new SourceSpan(triviaBeforeUrlStart, text.Start - 1); urlSpan.Start = text.Start; bool isAngleBracketsUrl = text.CurrentChar == '<'; unescapedUrl.Start = text.Start + (isAngleBracketsUrl ? 1 : 0); - if (!TryParseUrlWhitespace(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) + if (!TryParseUrlTrivia(ref text, out url, out urlHasPointyBrackets) || (!isAngleBracketsUrl && string.IsNullOrEmpty(url))) { return false; } urlSpan.End = text.Start - 1; unescapedUrl.End = text.Start - 1 - (isAngleBracketsUrl ? 1 : 0); - int whitespaceBeforeTitleStart = text.Start; + int triviaBeforeTitleStart = text.Start; var saved = text; var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out newline); - whitespaceBeforeTitle = new SourceSpan(whitespaceBeforeTitleStart, text.Start - 1); + triviaBeforeTitle = new SourceSpan(triviaBeforeTitleStart, text.Start - 1); var c = text.CurrentChar; if (c == '\'' || c == '"' || c == '(') { titleSpan.Start = text.Start; unescapedTitle.Start = text.Start + 1; // + 1; // skip opening enclosing character - if (TryParseTitleWhitespace(ref text, out title, out titleEnclosingCharacter)) + if (TryParseTitleTrivia(ref text, out title, out titleEnclosingCharacter)) { titleSpan.End = text.Start - 1; unescapedTitle.End = text.Start - 1 - 1; // skip closing enclosing character @@ -1264,14 +1264,14 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( { if (text.CurrentChar == '\0' || newLineCount > 0) { - whitespaceAfterTitle = new SourceSpan(text.Start, text.Start - 1); + triviaAfterTitle = new SourceSpan(text.Start, text.Start - 1); return true; } } // Check that the current line has only trailing spaces c = text.CurrentChar; - int whitespaceAfterTitleStart = text.Start; + int triviaAfterTitleStart = text.Start; while (c.IsSpaceOrTab()) { c = text.NextChar(); @@ -1286,7 +1286,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( text = saved; title = null; unescapedTitle = SourceSpan.Empty; - whitespaceAfterTitle = SourceSpan.Empty; + triviaAfterTitle = SourceSpan.Empty; return true; } @@ -1297,7 +1297,7 @@ public static bool TryParseLinkReferenceDefinitionWhitespace( unescapedTitle = SourceSpan.Empty; return false; } - whitespaceAfterTitle = new SourceSpan(whitespaceAfterTitleStart, text.Start - 1); + triviaAfterTitle = new SourceSpan(triviaAfterTitleStart, text.Start - 1); if (c != '\0') { if (c == '\n') @@ -1337,9 +1337,9 @@ public static bool TryParseLabel(ref T lines, out string label, out SourceSpa return TryParseLabel(ref lines, false, out label, out labelSpan); } - public static bool TryParseLabelWhitespace(ref T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator + public static bool TryParseLabelTrivia(ref T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator { - return TryParseLabelWhitespace(ref lines, false, out label, out labelSpan); + return TryParseLabelTrivia(ref lines, false, out label, out labelSpan); } public static bool TryParseLabel(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator @@ -1457,7 +1457,7 @@ public static bool TryParseLabel(ref T lines, bool allowEmpty, out string lab return isValid; } - public static bool TryParseLabelWhitespace(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator + public static bool TryParseLabelTrivia(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator { label = null; char c = lines.CurrentChar; diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 16c65341b..47ca0e674 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -169,11 +169,11 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo private bool ContinueProcessingLine { get; set; } /// - /// Gets or sets the position of the first character whitespace is encountered + /// Gets or sets the position of the first character trivia is encountered /// and not yet assigned to a syntax node. /// Trivia: only used when is enabled, otherwise 0. /// - public int WhitespaceStart { get; set; } + public int TriviaStart { get; set; } /// /// Returns trivia that has not yet been assigned to any node and @@ -181,10 +181,10 @@ public BlockProcessor(MarkdownDocument document, BlockParserList parsers, Markdo /// /// End position of the trivia /// - public StringSlice UseWhitespace(int end) + public StringSlice UseTrivia(int end) { - var stringSlice = new StringSlice(Line.Text, WhitespaceStart, end); - WhitespaceStart = end + 1; + var stringSlice = new StringSlice(Line.Text, TriviaStart, end); + TriviaStart = end + 1; return stringSlice; } @@ -203,7 +203,7 @@ internal List UseLinesBefore() /// Gets or sets the stack of empty lines not yet assigned to any . /// An entry may contain an empty . In that case the /// is relevant. Otherwise, the - /// entry will contain whitespace. + /// entry will contain trivia. /// public List LinesBefore { get; set; } @@ -382,7 +382,7 @@ public void UnwindAllIndents() var c = Line.PeekCharAbsolute(Line.Start - 1); // don't unwind all the way next to a '>', but one space right of the '>' if there is a space - if (TrackTrivia && SkipFirstUnwindSpace && Line.Start == WhitespaceStart) + if (TrackTrivia && SkipFirstUnwindSpace && Line.Start == TriviaStart) { break; } @@ -740,7 +740,7 @@ private void TryContinueBlocks() if (TrackTrivia) { LinesBefore ??= new List(); - var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); + var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.Newline); LinesBefore.Add(line); Line.Start = StartBeforeIndent; } @@ -820,7 +820,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (TrackTrivia) { LinesBefore ??= new List(); - var line = new StringSlice(Line.Text, WhitespaceStart, Line.Start - 1, Line.Newline); + var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.Newline); LinesBefore.Add(line); Line.Start = StartBeforeIndent; } @@ -878,8 +878,8 @@ private bool TryOpenBlocks(BlockParser[] parsers) // special case: take care when refactoring this if (paragraph.Parent is QuoteBlock qb) { - var whitespaceAfter = UseWhitespace(Start - 1); - qb.QuoteLines.Last().WhitespaceAfter = whitespaceAfter; + var triviaAfter = UseTrivia(Start - 1); + qb.QuoteLines.Last().TriviaAfter = triviaAfter; } } // We have just found a lazy continuation for a paragraph, early exit @@ -987,7 +987,7 @@ private void ResetLine(StringSlice newLine) ColumnBeforeIndent = 0; StartBeforeIndent = Start; originalLineStart = newLine.Start; - WhitespaceStart = newLine.Start; + TriviaStart = newLine.Start; } private void Reset() diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index 8b0ded91a..fe2ec54fc 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -159,13 +159,13 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String } end: - fenced.WhitespaceAfterFencedChar = afterFence; + fenced.TriviaAfterFencedChar = afterFence; fenced.Info = HtmlHelper.Unescape(info.ToString()); fenced.UnescapedInfo = info; - fenced.WhitespaceAfterInfo = afterInfo; + fenced.TriviaAfterInfo = afterInfo; fenced.Arguments = HtmlHelper.Unescape(arg.ToString()); fenced.UnescapedArguments = arg; - fenced.WhitespaceAfterArguments = afterArg; + fenced.TriviaAfterArguments = afterArg; fenced.InfoNewline = line.Newline; return true; @@ -332,8 +332,8 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var fencedBlock = block as IFencedBlock; fencedBlock.ClosingFencedCharCount = closingCount; fencedBlock.Newline = processor.Line.Newline; - fencedBlock.WhitespaceBeforeClosingFence = processor.UseWhitespace(sourcePosition - 1); - fencedBlock.WhitespaceAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); + fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1); + fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); // Don't keep the last line return BlockState.BreakDiscard; diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index 1ae774100..a24fef78f 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -31,7 +31,7 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) { IndentCount = processor.Indent, LinesBefore = processor.UseLinesBefore(), - WhitespaceBefore = processor.UseWhitespace(processor.Start - 1), + TriviaBefore = processor.UseTrivia(processor.Start - 1), Newline = processor.Line.Newline, }; } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index ec69c808c..983251f73 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -72,21 +72,21 @@ public override BlockState TryOpen(BlockProcessor processor) // A space is required after leading # if (leadingCount > 0 && leadingCount <= MaxLeadingCount && (c.IsSpaceOrTab() || c == '\0')) { - StringSlice whitespace = StringSlice.Empty; + StringSlice trivia = StringSlice.Empty; if (processor.TrackTrivia && c.IsSpaceOrTab()) { - whitespace = new StringSlice(processor.Line.Text, processor.Start, processor.Start); + trivia = new StringSlice(processor.Line.Text, processor.Start, processor.Start); processor.NextChar(); } // Move to the content var headingBlock = new HeadingBlock(this) { HeaderChar = matchingChar, - WhitespaceAfterAtxHeaderChar = whitespace, + TriviaAfterAtxHeaderChar = trivia, Level = leadingCount, Column = column, Span = { Start = sourcePosition }, - WhitespaceBefore = processor.UseWhitespace(sourcePosition - 1), + TriviaBefore = processor.UseTrivia(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), Newline = processor.Line.Newline, }; @@ -143,11 +143,11 @@ public override BlockState TryOpen(BlockProcessor processor) if (processor.TrackTrivia) { var wsa = new StringSlice(processor.Line.Text, processor.Line.End + 1, sourceEnd); - headingBlock.WhitespaceAfter = wsa; - if (wsa.Overlaps(headingBlock.WhitespaceAfterAtxHeaderChar)) + headingBlock.TriviaAfter = wsa; + if (wsa.Overlaps(headingBlock.TriviaAfterAtxHeaderChar)) { // prevent double whitespace allocation in case of closing # i.e. "# #" - headingBlock.WhitespaceAfterAtxHeaderChar = StringSlice.Empty; + headingBlock.TriviaAfterAtxHeaderChar = StringSlice.Empty; } } diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index 48ddc91c1..ea7210026 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -41,7 +41,7 @@ public override BlockState TryOpen(BlockProcessor processor) }; var codeBlockLine = new CodeBlockLine { - WhitespaceBefore = processor.UseWhitespace(sourceStartPosition - 1) + TriviaBefore = processor.UseTrivia(sourceStartPosition - 1) }; codeBlock.CodeBlockLines.Add(codeBlockLine); processor.NewBlocks.Push(codeBlock); @@ -94,7 +94,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var cb = (CodeBlock)block; var codeBlockLine = new CodeBlockLine { - WhitespaceBefore = processor.UseWhitespace(processor.Start - 1) + TriviaBefore = processor.UseTrivia(processor.Start - 1) }; cb.CodeBlockLines ??= new List(); cb.CodeBlockLines.Add(codeBlockLine); @@ -126,7 +126,7 @@ public override bool Close(BlockProcessor processor, Block block) if (processor.TrackTrivia) { var quoteLine = codeBlock.CodeBlockLines[i]; - var emptyLine = new StringSlice(line.Slice.Text, quoteLine.WhitespaceBefore.Start, line.Slice.End, line.Newline); + var emptyLine = new StringSlice(line.Slice.Text, quoteLine.TriviaBefore.Start, line.Slice.End, line.Newline); block.LinesAfter ??= new List(); block.LinesAfter.Add(emptyLine); } diff --git a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs index 418c00a75..0b482abd5 100644 --- a/src/Markdig/Parsers/Inlines/LinkInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LinkInlineParser.cs @@ -42,7 +42,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } } string label; - SourceSpan labelWithWhitespaceSpan = SourceSpan.Empty; + SourceSpan labelWithTriviaSpan = SourceSpan.Empty; switch (c) { case '[': @@ -54,10 +54,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // If the label is followed by either a ( or a [, this is not a shortcut if (processor.TrackTrivia) { - if (LinkHelper.TryParseLabelWhitespace(ref slice, out label, out labelSpan)) + if (LinkHelper.TryParseLabelTrivia(ref slice, out label, out labelSpan)) { - labelWithWhitespaceSpan.Start = labelSpan.Start; // skip opening [ - labelWithWhitespaceSpan.End = labelSpan.End; // skip closing ] + labelWithTriviaSpan.Start = labelSpan.Start; // skip opening [ + labelWithTriviaSpan.End = labelSpan.End; // skip closing ] if (!processor.Document.ContainsLinkReferenceDefinition(label)) { label = null; @@ -78,13 +78,12 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // Else we insert a LinkDelimiter slice.NextChar(); - //labelWithWhitespaceSpan = processor.GetSourcePositionFromLocalSpan(labelWithWhitespaceSpan); - var labelWithWhitespace = new StringSlice(slice.Text, labelWithWhitespaceSpan.Start, labelWithWhitespaceSpan.End); + var labelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End); processor.Inline = new LinkDelimiterInline(this) { Type = DelimiterType.Open, Label = label, - LabelWithWhitespace = labelWithWhitespace, + LabelWithTrivia = labelWithTrivia, LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan), IsImage = isImage, Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)), @@ -116,7 +115,7 @@ private bool ProcessLinkReference( InlineProcessor state, StringSlice text, string label, - SourceSpan labelWithWhitespaceSpan, + SourceSpan labelWithriviaSpan, bool isShortcut, SourceSpan labelSpan, LinkDelimiterInline parent, @@ -138,7 +137,7 @@ private bool ProcessLinkReference( // Create a default link if the callback was not found if (link == null) { - var labelWithWhitespace = new StringSlice(text.Text, labelWithWhitespaceSpan.Start, labelWithWhitespaceSpan.End); + var labelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End); // Inline Link link = new LinkInline() { @@ -146,9 +145,9 @@ private bool ProcessLinkReference( Title = HtmlHelper.Unescape(linkRef.Title), Label = label, LabelSpan = labelSpan, - LabelWithWhitespace = labelWithWhitespace, + LabelWithTrivia = labelWithTrivia, LinkRefDefLabel = linkRef.Label, - LinkRefDefLabelWithWhitespace = linkRef.LabelWithWhitespace, + LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia, LocalLabel = localLabel, UrlSpan = linkRef.UrlSpan, IsImage = parent.IsImage, @@ -236,7 +235,7 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice { if (inlineState.TrackTrivia) { - if (LinkHelper.TryParseInlineLinkWhitespace( + if (LinkHelper.TryParseInlineLinkTrivia( ref text, out string url, out SourceSpan unescapedUrlSpan, @@ -245,28 +244,28 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice out char titleEnclosingCharacter, out SourceSpan linkSpan, out SourceSpan titleSpan, - out SourceSpan whitespaceBeforeLink, - out SourceSpan whitespaceAfterLink, - out SourceSpan whitespaceAfterTitle, + out SourceSpan triviaBeforeLink, + out SourceSpan triviaAfterLink, + out SourceSpan triviaAfterTitle, out bool urlHasPointyBrackets)) { - var wsBeforeLink = new StringSlice(text.Text, whitespaceBeforeLink.Start, whitespaceBeforeLink.End); - var wsAfterLink = new StringSlice(text.Text, whitespaceAfterLink.Start, whitespaceAfterLink.End); - var wsAfterTitle = new StringSlice(text.Text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End); + var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End); + var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End); var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End); var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End); // Inline Link var link = new LinkInline() { - WhitespaceBeforeUrl = wsBeforeLink, + TriviaBeforeUrl = wsBeforeLink, Url = HtmlHelper.Unescape(url), UnescapedUrl = unescapedUrl, UrlHasPointyBrackets = urlHasPointyBrackets, - WhitespaceAfterUrl = wsAfterLink, + TriviaAfterUrl = wsAfterLink, Title = HtmlHelper.Unescape(title), UnescapedTitle = unescapedTitle, TitleEnclosingCharacter = titleEnclosingCharacter, - WhitespaceAfterTitle = wsAfterTitle, + TriviaAfterTitle = wsAfterTitle, IsImage = openParent.IsImage, LabelSpan = openParent.LabelSpan, UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan), @@ -340,7 +339,7 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice var labelSpan = SourceSpan.Empty; string label = null; - SourceSpan labelWithWhitespace = SourceSpan.Empty; + SourceSpan labelWithTrivia = SourceSpan.Empty; bool isLabelSpanLocal = true; bool isShortcut = false; @@ -364,15 +363,15 @@ private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice label = openParent.Label; isShortcut = true; } - if (label != null || LinkHelper.TryParseLabelWhitespace(ref text, true, out label, out labelSpan)) + if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan)) { - labelWithWhitespace = new SourceSpan(labelSpan.Start, labelSpan.End); + labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End); if (isLabelSpanLocal) { labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan); } - if (ProcessLinkReference(inlineState, text, label, labelWithWhitespace, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) + if (ProcessLinkReference(inlineState, text, label, labelWithTrivia, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1), localLabel)) { // Remove the open parent openParent.Remove(); diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 7831e7a2e..78732409c 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -209,11 +209,11 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) state.GoToColumn(initColumn); return BlockState.None; } - var savedWhitespaceStart = state.WhitespaceStart; - var whitespaceBefore = state.UseWhitespace(sourcePosition - 1); + var savedTriviaStart = state.TriviaStart; + var triviaBefore = state.UseTrivia(sourcePosition - 1); - // set whitespace to the mandatory whitespace after the bullet - state.WhitespaceStart = state.Start; + // set trivia to the mandatory whitespace after the bullet + state.TriviaStart = state.Start; bool isOrdered = itemParser is OrderedListItemParser; @@ -234,7 +234,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) if (!c.IsSpaceOrTab()) { state.GoToColumn(initColumn); - state.WhitespaceStart = savedWhitespaceStart; // restore changed WhitespaceStart state + state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state return BlockState.None; } @@ -265,7 +265,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) state.IsOpen(previousParagraph) && listInfo.BulletType == '1' && listInfo.OrderedStart != "1") { state.GoToColumn(initColumn); - state.WhitespaceStart = savedWhitespaceStart; // restore changed WhitespaceStart state + state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state return BlockState.None; } } @@ -277,7 +277,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) ColumnWidth = columnWidth, Order = order, SourceBullet = listInfo.SourceBullet, - WhitespaceBefore = whitespaceBefore, + TriviaBefore = triviaBefore, Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), Newline = state.Line.Newline, diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 6396bd824..2615485b4 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -58,7 +58,7 @@ public override bool Close(BlockProcessor processor, Block block) if (processor.TrackTrivia) { - TryMatchLinkReferenceDefinitionWhitespace(ref lines, processor, paragraph); + TryMatchLinkReferenceDefinitionTrivia(ref lines, processor, paragraph); } else { @@ -100,7 +100,7 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) bool foundLrd; if (state.TrackTrivia) { - foundLrd = TryMatchLinkReferenceDefinitionWhitespace(ref paragraph.Lines, state, paragraph); + foundLrd = TryMatchLinkReferenceDefinitionTrivia(ref paragraph.Lines, state, paragraph); } else { @@ -129,8 +129,8 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) Span = new SourceSpan(paragraph.Span.Start, line.Start), Level = level, Lines = paragraph.Lines, - WhitespaceBefore = state.UseWhitespace(sourcePosition - 1), // remove dashes - WhitespaceAfter = new StringSlice(state.Line.Text, state.Start, line.End), + TriviaBefore = state.UseTrivia(sourcePosition - 1), // remove dashes + TriviaAfter = new StringSlice(state.Line.Text, state.Start, line.End), LinesBefore = paragraph.LinesBefore, Newline = state.Line.Newline, IsSetext = true, @@ -213,7 +213,7 @@ private static bool TryMatchLinkReferenceDefinition(ref StringLineGroup lines, B return atLeastOneFound; } - private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGroup lines, BlockProcessor state, ParagraphBlock paragraph) + private static bool TryMatchLinkReferenceDefinitionTrivia(ref StringLineGroup lines, BlockProcessor state, ParagraphBlock paragraph) { bool atLeastOneFound = false; @@ -221,16 +221,16 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou { // If we have found a LinkReferenceDefinition, we can discard the previous paragraph var iterator = lines.ToCharIterator(); - if (LinkReferenceDefinition.TryParseWhitespace( + if (LinkReferenceDefinition.TryParseTrivia( ref iterator, out LinkReferenceDefinition lrd, - out SourceSpan whitespaceBeforeLabel, - out SourceSpan labelWithWhitespace, - out SourceSpan whitespaceBeforeUrl, + out SourceSpan triviaBeforeLabel, + out SourceSpan labelWithTrivia, + out SourceSpan triviaBeforeUrl, out SourceSpan unescapedUrl, - out SourceSpan whitespaceBeforeTitle, + out SourceSpan triviaBeforeTitle, out SourceSpan unescapedTitle, - out SourceSpan whitespaceAfterTitle)) + out SourceSpan triviaAfterTitle)) { state.Document.SetLinkReferenceDefinition(lrd.Label, lrd, false); lrd.Parent = null; // remove LRDG parent from lrd @@ -241,24 +241,24 @@ private static bool TryMatchLinkReferenceDefinitionWhitespace(ref StringLineGrou var text = lines.Lines[0].Slice.Text; int startPosition = lines.Lines[0].Slice.Start; - whitespaceBeforeLabel = whitespaceBeforeLabel.MoveForward(startPosition); - labelWithWhitespace = labelWithWhitespace.MoveForward(startPosition); - whitespaceBeforeUrl = whitespaceBeforeUrl.MoveForward(startPosition); + triviaBeforeLabel = triviaBeforeLabel.MoveForward(startPosition); + labelWithTrivia = labelWithTrivia.MoveForward(startPosition); + triviaBeforeUrl = triviaBeforeUrl.MoveForward(startPosition); unescapedUrl = unescapedUrl.MoveForward(startPosition); - whitespaceBeforeTitle = whitespaceBeforeTitle.MoveForward(startPosition); + triviaBeforeTitle = triviaBeforeTitle.MoveForward(startPosition); unescapedTitle = unescapedTitle.MoveForward(startPosition); - whitespaceAfterTitle = whitespaceAfterTitle.MoveForward(startPosition); + triviaAfterTitle = triviaAfterTitle.MoveForward(startPosition); lrd.Span = lrd.Span.MoveForward(startPosition); - lrd.WhitespaceBefore = new StringSlice(text, whitespaceBeforeLabel.Start, whitespaceBeforeLabel.End); + lrd.TriviaBefore = new StringSlice(text, triviaBeforeLabel.Start, triviaBeforeLabel.End); lrd.LabelSpan = lrd.LabelSpan.MoveForward(startPosition); - lrd.LabelWithWhitespace = new StringSlice(text, labelWithWhitespace.Start, labelWithWhitespace.End); - lrd.WhitespaceBeforeUrl = new StringSlice(text, whitespaceBeforeUrl.Start, whitespaceBeforeUrl.End); + lrd.LabelWithTrivia = new StringSlice(text, labelWithTrivia.Start, labelWithTrivia.End); + lrd.TriviaBeforeUrl = new StringSlice(text, triviaBeforeUrl.Start, triviaBeforeUrl.End); lrd.UrlSpan = lrd.UrlSpan.MoveForward(startPosition); lrd.UnescapedUrl = new StringSlice(text, unescapedUrl.Start, unescapedUrl.End); - lrd.WhitespaceBeforeTitle = new StringSlice(text, whitespaceBeforeTitle.Start, whitespaceBeforeTitle.End); + lrd.TriviaBeforeTitle = new StringSlice(text, triviaBeforeTitle.Start, triviaBeforeTitle.End); lrd.TitleSpan = lrd.TitleSpan.MoveForward(startPosition); lrd.UnescapedTitle = new StringSlice(text, unescapedTitle.Start, unescapedTitle.End); - lrd.WhitespaceAfter = new StringSlice(text, whitespaceAfterTitle.Start, whitespaceAfterTitle.End); + lrd.TriviaAfter = new StringSlice(text, triviaAfterTitle.Start, triviaAfterTitle.End); lrd.LinesBefore = paragraph.LinesBefore; state.LinesBefore = paragraph.LinesAfter; // ensure closed paragraph with linesafter placed back on stack diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index 0769de0bd..eedfefb99 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -57,19 +57,19 @@ public override BlockState TryOpen(BlockProcessor processor) processor.NextColumn(); } - var whitespaceBefore = processor.UseWhitespace(sourcePosition - 1); - StringSlice whitespaceAfter = StringSlice.Empty; + var triviaBefore = processor.UseTrivia(sourcePosition - 1); + StringSlice triviaAfter = StringSlice.Empty; bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) { - processor.WhitespaceStart = processor.Start; - whitespaceAfter = processor.UseWhitespace(processor.Line.End); + processor.TriviaStart = processor.Start; + triviaAfter = processor.UseTrivia(processor.Line.End); wasEmptyLine = true; } quoteBlock.QuoteLines.Add(new QuoteBlockLine { - WhitespaceBefore = whitespaceBefore, - WhitespaceAfter = whitespaceAfter, + TriviaBefore = triviaBefore, + TriviaAfter = triviaAfter, QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, Newline = processor.Line.Newline, @@ -77,7 +77,7 @@ public override BlockState TryOpen(BlockProcessor processor) processor.NewBlocks.Push(quoteBlock); if (!wasEmptyLine) { - processor.WhitespaceStart = processor.Start; + processor.TriviaStart = processor.Start; } return BlockState.Continue; } @@ -124,27 +124,27 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { processor.NextColumn(); } - var whiteSpaceBefore = processor.UseWhitespace(sourcePosition - 1); - StringSlice whitespaceAfter = StringSlice.Empty; + var TriviaSpaceBefore = processor.UseTrivia(sourcePosition - 1); + StringSlice triviaAfter = StringSlice.Empty; bool wasEmptyLine = false; if (processor.Line.IsEmptyOrWhitespace()) { - processor.WhitespaceStart = processor.Start; - whitespaceAfter = processor.UseWhitespace(processor.Line.End); + processor.TriviaStart = processor.Start; + triviaAfter = processor.UseTrivia(processor.Line.End); wasEmptyLine = true; } quote.QuoteLines.Add(new QuoteBlockLine { QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, - WhitespaceBefore = whiteSpaceBefore, - WhitespaceAfter = whitespaceAfter, + TriviaBefore = TriviaSpaceBefore, + TriviaAfter = triviaAfter, Newline = processor.Line.Newline, }); if (!wasEmptyLine) { - processor.WhitespaceStart = processor.Start; + processor.TriviaStart = processor.Start; } block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index 6d3b86798..5e99e87fd 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -95,7 +95,7 @@ public override BlockState TryOpen(BlockProcessor processor) //BeforeWhitespace = beforeWhitespace, //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), LinesBefore = processor.UseLinesBefore(), - Content = new StringSlice(line.Text, processor.WhitespaceStart, line.End, line.Newline), //include whitespace for now + Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.Newline), //include whitespace for now Newline = processor.Line.Newline, }); return BlockState.BreakDiscard; diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index 823e37755..1e24f9abc 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -20,29 +20,29 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) renderer.RenderLinesBefore(obj); if (obj is FencedCodeBlock fencedCodeBlock) { - renderer.Write(obj.WhitespaceBefore); + renderer.Write(obj.TriviaBefore); var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount); renderer.Write(opening); - if (!fencedCodeBlock.WhitespaceAfterFencedChar.IsEmpty) + if (!fencedCodeBlock.TriviaAfterFencedChar.IsEmpty) { - renderer.Write(fencedCodeBlock.WhitespaceAfterFencedChar); + renderer.Write(fencedCodeBlock.TriviaAfterFencedChar); } if (fencedCodeBlock.Info != null) { renderer.Write(fencedCodeBlock.UnescapedInfo); } - if (!fencedCodeBlock.WhitespaceAfterInfo.IsEmpty) + if (!fencedCodeBlock.TriviaAfterInfo.IsEmpty) { - renderer.Write(fencedCodeBlock.WhitespaceAfterInfo); + renderer.Write(fencedCodeBlock.TriviaAfterInfo); } if (!string.IsNullOrEmpty(fencedCodeBlock.Arguments)) { renderer.Write(fencedCodeBlock.UnescapedArguments); } - if (!fencedCodeBlock.WhitespaceAfterArguments.IsEmpty) + if (!fencedCodeBlock.TriviaAfterArguments.IsEmpty) { - renderer.Write(fencedCodeBlock.WhitespaceAfterArguments); + renderer.Write(fencedCodeBlock.TriviaAfterArguments); } /* TODO do we need this causes a empty space and would render html attributes to markdown. @@ -57,7 +57,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) renderer.WriteLeafRawLines(obj); - renderer.Write(fencedCodeBlock.WhitespaceBeforeClosingFence); + renderer.Write(fencedCodeBlock.TriviaBeforeClosingFence); var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount); renderer.Write(closing); if (!string.IsNullOrEmpty(closing)) @@ -65,14 +65,14 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) // See example 207: "> ```\nfoo\n```" renderer.WriteLine(obj.Newline); } - renderer.Write(obj.WhitespaceAfter); + renderer.Write(obj.TriviaAfter); } else { var indents = new List(); foreach (var cbl in obj.CodeBlockLines) { - indents.Add(cbl.WhitespaceBefore.ToString()); + indents.Add(cbl.TriviaBefore.ToString()); } renderer.PushIndent(indents); WriteLeafRawLines(renderer, obj); diff --git a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs index 2f967d327..9fae3fb55 100644 --- a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs @@ -32,10 +32,10 @@ protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) renderer.WriteLeafInline(obj); renderer.WriteLine(obj.SetextNewline); - renderer.Write(obj.WhitespaceBefore); + renderer.Write(obj.TriviaBefore); renderer.Write(line); renderer.WriteLine(obj.Newline); - renderer.Write(obj.WhitespaceAfter); + renderer.Write(obj.TriviaAfter); renderer.RenderLinesAfter(obj); } @@ -47,11 +47,11 @@ protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) ? HeadingTexts[obj.Level - 1] : new string('#', obj.Level); - renderer.Write(obj.WhitespaceBefore); + renderer.Write(obj.TriviaBefore); renderer.Write(headingText); - renderer.Write(obj.WhitespaceAfterAtxHeaderChar); + renderer.Write(obj.TriviaAfterAtxHeaderChar); renderer.WriteLeafInline(obj); - renderer.Write(obj.WhitespaceAfter); + renderer.Write(obj.TriviaAfter); renderer.WriteLine(obj.Newline); renderer.RenderLinesAfter(obj); diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs index 7a7cd3696..985a94c73 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LinkInlineRenderer.cs @@ -30,7 +30,7 @@ protected override void Write(RoundtripRenderer renderer, LinkInline link) renderer.Write('['); if (link.LocalLabel == LocalLabel.Local) { - renderer.Write(link.LabelWithWhitespace); + renderer.Write(link.LabelWithTrivia); } renderer.Write(']'); } @@ -40,7 +40,7 @@ protected override void Write(RoundtripRenderer renderer, LinkInline link) if (link.Url != null) { renderer.Write('('); - renderer.Write(link.WhitespaceBeforeUrl); + renderer.Write(link.TriviaBeforeUrl); if (link.UrlHasPointyBrackets) { renderer.Write('<'); @@ -50,7 +50,7 @@ protected override void Write(RoundtripRenderer renderer, LinkInline link) { renderer.Write('>'); } - renderer.Write(link.WhitespaceAfterUrl); + renderer.Write(link.TriviaAfterUrl); if (!string.IsNullOrEmpty(link.Title)) { @@ -63,7 +63,7 @@ protected override void Write(RoundtripRenderer renderer, LinkInline link) renderer.Write(open); renderer.Write(link.UnescapedTitle); renderer.Write(close); - renderer.Write(link.WhitespaceAfterTitle); + renderer.Write(link.TriviaAfterTitle); } renderer.Write(')'); diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs index 0520751e9..43704a6c0 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -13,12 +13,12 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio { renderer.RenderLinesBefore(linkDef); - renderer.Write(linkDef.WhitespaceBefore); + renderer.Write(linkDef.TriviaBefore); renderer.Write('['); - renderer.Write(linkDef.LabelWithWhitespace); + renderer.Write(linkDef.LabelWithTrivia); renderer.Write("]:"); - renderer.Write(linkDef.WhitespaceBeforeUrl); + renderer.Write(linkDef.TriviaBeforeUrl); if (linkDef.UrlHasPointyBrackets) { renderer.Write('<'); @@ -29,7 +29,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio renderer.Write('>'); } - renderer.Write(linkDef.WhitespaceBeforeTitle); + renderer.Write(linkDef.TriviaBeforeTitle); if (linkDef.Title != null) { var open = linkDef.TitleEnclosingCharacter; @@ -42,7 +42,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio renderer.Write(linkDef.UnescapedTitle); renderer.Write(close); } - renderer.Write(linkDef.WhitespaceAfter); + renderer.Write(linkDef.TriviaAfter); renderer.Write(linkDef.Newline.AsString()); renderer.RenderLinesAfter(linkDef); diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index e703dcc1c..e08c252fc 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -25,7 +25,7 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); - var bws = listItem.WhitespaceBefore.ToString(); + var bws = listItem.TriviaBefore.ToString(); var bullet = listItem.SourceBullet.ToString(); var delimiter = listBlock.OrderedDelimiter; renderer.PushIndent(new List { $@"{bws}{bullet}{delimiter}" }); @@ -41,9 +41,9 @@ protected override void Write(RoundtripRenderer renderer, ListBlock listBlock) var listItem = (ListItemBlock) item; renderer.RenderLinesBefore(listItem); - StringSlice bws = listItem.WhitespaceBefore; + StringSlice bws = listItem.TriviaBefore; char bullet = listBlock.BulletType; - StringSlice aws = listItem.WhitespaceAfter; + StringSlice aws = listItem.TriviaAfter; renderer.PushIndent(new List { $@"{bws}{bullet}{aws}" }); if (listItem.Count == 0) diff --git a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs index 753d2514f..c0b8ed44b 100644 --- a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs @@ -17,7 +17,7 @@ public class ParagraphRenderer : RoundtripObjectRenderer protected override void Write(RoundtripRenderer renderer, ParagraphBlock paragraph) { renderer.RenderLinesBefore(paragraph); - renderer.Write(paragraph.WhitespaceBefore); + renderer.Write(paragraph.TriviaBefore); renderer.WriteLeafInline(paragraph); //renderer.Write(paragraph.Newline); // paragraph typically has LineBreakInlines as closing inline nodes renderer.RenderLinesAfter(paragraph); diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index a4ce46f66..fda66725a 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -17,15 +17,15 @@ public class QuoteBlockRenderer : RoundtripObjectRenderer protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) { renderer.RenderLinesBefore(quoteBlock); - renderer.Write(quoteBlock.WhitespaceBefore); + renderer.Write(quoteBlock.TriviaBefore); var indents = new List(); foreach (var quoteLine in quoteBlock.QuoteLines) { - var wsb = quoteLine.WhitespaceBefore.ToString(); + var wsb = quoteLine.TriviaBefore.ToString(); var quoteChar = quoteLine.QuoteChar ? ">" : ""; var spaceAfterQuoteChar = quoteLine.HasSpaceAfterQuoteChar ? " " : ""; - var wsa = quoteLine.WhitespaceAfter.ToString(); + var wsa = quoteLine.TriviaAfter.ToString(); indents.Add(wsb + quoteChar + spaceAfterQuoteChar + wsa); } bool noChildren = false; diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index 8d98ced2b..dc1fd56c2 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -56,18 +56,18 @@ protected Block(BlockParser parser) public bool RemoveAfterProcessInlines { get; set; } /// - /// Gets or sets the whitespace right before this block. + /// Gets or sets the trivia right before this block. /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceBefore { get; set; } + public StringSlice TriviaBefore { get; set; } /// - /// Gets or sets whitespace occurring after this block. + /// Gets or sets trivia occurring after this block. /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceAfter { get; set; } + public StringSlice TriviaAfter { get; set; } /// /// Gets or sets the empty lines occurring before this block. diff --git a/src/Markdig/Syntax/CodeBlock.cs b/src/Markdig/Syntax/CodeBlock.cs index 2c2296d56..65743057f 100644 --- a/src/Markdig/Syntax/CodeBlock.cs +++ b/src/Markdig/Syntax/CodeBlock.cs @@ -18,7 +18,7 @@ public class CodeBlock : LeafBlock { public class CodeBlockLine { - public StringSlice WhitespaceBefore { get; set; } + public StringSlice TriviaBefore { get; set; } } public List CodeBlockLines { get; set; } = new List(); diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index b75ab9e46..3262b4087 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -41,7 +41,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) public int OpeningFencedCharCount { get; set; } /// - public StringSlice WhitespaceAfterFencedChar { get; set; } + public StringSlice TriviaAfterFencedChar { get; set; } /// public string Info { get; set; } @@ -50,7 +50,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) public StringSlice UnescapedInfo { get; set; } /// - public StringSlice WhitespaceAfterInfo { get; set; } + public StringSlice TriviaAfterInfo { get; set; } /// public string Arguments { get; set; } @@ -59,13 +59,13 @@ public FencedCodeBlock(BlockParser parser) : base(parser) public StringSlice UnescapedArguments { get; set; } /// - public StringSlice WhitespaceAfterArguments { get; set; } + public StringSlice TriviaAfterArguments { get; set; } /// public Newline InfoNewline { get; set; } /// - public StringSlice WhitespaceBeforeClosingFence { get; set; } + public StringSlice TriviaBeforeClosingFence { get; set; } /// public int ClosingFencedCharCount { get; set; } diff --git a/src/Markdig/Syntax/HeadingBlock.cs b/src/Markdig/Syntax/HeadingBlock.cs index eadc76d81..f4e6417c5 100644 --- a/src/Markdig/Syntax/HeadingBlock.cs +++ b/src/Markdig/Syntax/HeadingBlock.cs @@ -53,6 +53,6 @@ public HeadingBlock(BlockParser parser) : base(parser) /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceAfterAtxHeaderChar { get; set; } + public StringSlice TriviaAfterAtxHeaderChar { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IBlock.cs b/src/Markdig/Syntax/IBlock.cs index d3d1435a4..ee79549c0 100644 --- a/src/Markdig/Syntax/IBlock.cs +++ b/src/Markdig/Syntax/IBlock.cs @@ -58,8 +58,8 @@ public interface IBlock : IMarkdownObject /// event ProcessInlineDelegate ProcessInlinesEnd; - public StringSlice WhitespaceBefore { get; set; } - public StringSlice WhitespaceAfter { get; set; } + public StringSlice TriviaBefore { get; set; } + public StringSlice TriviaAfter { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index 7bdcc90c9..f7b3b9773 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -23,11 +23,11 @@ public interface IFencedBlock : IBlock public int OpeningFencedCharCount { get; set; } /// - /// Gets or sets the whitespace after the . + /// Gets or sets the trivia after the . /// Trivia: only parsed when is enabled, otherwise /// . /// - StringSlice WhitespaceAfterFencedChar { get; set; } + StringSlice TriviaAfterFencedChar { get; set; } /// /// Gets or sets the language parsed after the first line of @@ -43,11 +43,11 @@ public interface IFencedBlock : IBlock StringSlice UnescapedInfo { get; set; } /// - /// Gets or sets the whitespace after the . + /// Gets or sets the trivia after the . /// Trivia: only parsed when is enabled, otherwise /// . /// - StringSlice WhitespaceAfterInfo { get; set; } + StringSlice TriviaAfterInfo { get; set; } /// /// Gets or sets the arguments after the . @@ -63,11 +63,11 @@ public interface IFencedBlock : IBlock StringSlice UnescapedArguments { get; set; } /// - /// Gets or sets the whitespace after the . + /// Gets or sets the trivia after the . /// Trivia: only parsed when is enabled, otherwise /// . /// - StringSlice WhitespaceAfterArguments { get; set; } + StringSlice TriviaAfterArguments { get; set; } /// /// Newline of the line with the opening fenced chars. @@ -77,11 +77,11 @@ public interface IFencedBlock : IBlock Newline InfoNewline { get; set; } /// - /// Whitespace before the closing fenced chars + /// Trivia before the closing fenced chars /// Trivia: only parsed when is enabled, otherwise /// . /// - StringSlice WhitespaceBeforeClosingFence { get; set; } + StringSlice TriviaBeforeClosingFence { get; set; } /// /// Gets or sets the fenced character count used to close this fenced code block. diff --git a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs index 3503b54ed..3d7ccb98b 100644 --- a/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkDelimiterInline.cs @@ -33,11 +33,11 @@ public LinkDelimiterInline(InlineParser parser) : base(parser) public SourceSpan LabelSpan; /// - /// Gets or sets the with whitespace. + /// Gets or sets the with trivia. /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice LabelWithWhitespace { get; set; } + public StringSlice LabelWithTrivia { get; set; } public override string ToLiteral() { diff --git a/src/Markdig/Syntax/Inlines/LinkInline.cs b/src/Markdig/Syntax/Inlines/LinkInline.cs index a1bf62f4d..09e268a2e 100644 --- a/src/Markdig/Syntax/Inlines/LinkInline.cs +++ b/src/Markdig/Syntax/Inlines/LinkInline.cs @@ -61,11 +61,11 @@ public LinkInline(string url, string title) public SourceSpan? LabelSpan; /// - /// Gets or sets the with whitespace. + /// Gets or sets the with trivia. /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice LabelWithWhitespace { get; set; } + public StringSlice LabelWithTrivia { get; set; } /// /// Gets or sets the type of label parsed @@ -85,17 +85,17 @@ public LinkInline(string url, string title) public string LinkRefDefLabel { get; set; } /// - /// Gets or sets the with whitespace as matched against + /// Gets or sets the with trivia as matched against /// the /// - public StringSlice LinkRefDefLabelWithWhitespace { get; set; } + public StringSlice LinkRefDefLabelWithTrivia { get; set; } /// - /// Gets or sets the whitespace before the . + /// Gets or sets the trivia before the . /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceBeforeUrl { get; set; } + public StringSlice TriviaBeforeUrl { get; set; } /// /// True if the in the source document is enclosed @@ -116,18 +116,18 @@ public LinkInline(string url, string title) public SourceSpan? UrlSpan; /// - /// The but with whitespace and unescaped characters + /// The but with trivia and unescaped characters /// Trivia: only parsed when is enabled, otherwise /// . /// public StringSlice UnescapedUrl { get; set; } /// - /// Any whitespace after the . + /// Any trivia after the . /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceAfterUrl { get; set; } + public StringSlice TriviaAfterUrl { get; set; } /// /// Gets or sets the GetDynamicUrl delegate. If this property is set, @@ -161,11 +161,11 @@ public LinkInline(string url, string title) public StringSlice UnescapedTitle { get; set; } /// - /// Gets or sets the whitespace after the . + /// Gets or sets the trivia after the . /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceAfterTitle { get; set; } + public StringSlice TriviaAfterTitle { get; set; } /// /// Gets or sets a boolean indicating if this link is a shortcut link to a diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index baad2f3fa..8b7caf6a3 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -56,18 +56,18 @@ public LinkReferenceDefinition(string label, string url, string title) : this() public SourceSpan LabelSpan; /// - /// Non-normalized Label (includes whitespace) + /// Non-normalized Label (includes trivia) /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice LabelWithWhitespace { get; set; } + public StringSlice LabelWithTrivia { get; set; } /// /// Whitespace before the . /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceBeforeUrl { get; set; } + public StringSlice TriviaBeforeUrl { get; set; } /// /// Gets or sets the URL. @@ -98,7 +98,7 @@ public LinkReferenceDefinition(string label, string url, string title) : this() /// Trivia: only parsed when is enabled, otherwise /// . /// - public StringSlice WhitespaceBeforeTitle { get; set; } + public StringSlice TriviaBeforeTitle { get; set; } /// /// Gets or sets the title. @@ -166,36 +166,36 @@ public static bool TryParse(ref T text, out LinkReferenceDefinition block) wh /// The text. /// The block. /// true if parsing is successful; false otherwise - public static bool TryParseWhitespace( + public static bool TryParseTrivia( ref T text, out LinkReferenceDefinition block, - out SourceSpan whitespaceBeforeLabel, - out SourceSpan labelWithWhitespace, - out SourceSpan whitespaceBeforeUrl, + out SourceSpan triviaBeforeLabel, + out SourceSpan labelWithTrivia, + out SourceSpan triviaBeforeUrl, out SourceSpan unescapedUrl, - out SourceSpan whitespaceBeforeTitle, + out SourceSpan triviaBeforeTitle, out SourceSpan unescapedTitle, - out SourceSpan whitespaceAfterTitle) where T : ICharIterator + out SourceSpan triviaAfterTitle) where T : ICharIterator { block = null; var startSpan = text.Start; - if (!LinkHelper.TryParseLinkReferenceDefinitionWhitespace( + if (!LinkHelper.TryParseLinkReferenceDefinitionTrivia( ref text, - out whitespaceBeforeLabel, + out triviaBeforeLabel, out string label, - out labelWithWhitespace, - out whitespaceBeforeUrl, + out labelWithTrivia, + out triviaBeforeUrl, out string url, out unescapedUrl, out bool urlHasPointyBrackets, - out whitespaceBeforeTitle, + out triviaBeforeTitle, out string title, out unescapedTitle, out char titleEnclosingCharacter, out Newline newline, - out whitespaceAfterTitle, + out triviaAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan)) diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index a9592e0eb..e8c561c19 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -43,11 +43,11 @@ public QuoteBlock(BlockParser parser) : base(parser) public class QuoteBlockLine { /// - /// Gets or sets whitespace occuring before the first quote character. + /// Gets or sets trivia occuring before the first quote character. /// Trivia: only parsed when is enabled, otherwise /// is empty. /// - public StringSlice WhitespaceBefore { get; set; } + public StringSlice TriviaBefore { get; set; } /// /// True when this QuoteBlock line has a quote character. False when @@ -65,13 +65,13 @@ public class QuoteBlockLine public bool HasSpaceAfterQuoteChar { get; set; } /// - /// Gets or sets the whitespace after the the space after the quote character. + /// Gets or sets the trivia after the the space after the quote character. /// The first space is assigned to , subsequent - /// whitespace is assigned to this property. + /// trivia is assigned to this property. /// Trivia: only parsed when is enabled, otherwise /// is empty. /// - public StringSlice WhitespaceAfter { get; set; } + public StringSlice TriviaAfter { get; set; } /// /// Gets or sets the newline of this QuoeBlockLine. From 383b197490012ae94740e8ef7dd7173a3dce614a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Mon, 1 Mar 2021 23:07:40 +0100 Subject: [PATCH 111/120] document roundtrip parsing feature --- readme.md | 1 + src/Markdig.Tests/Roundtrip.md | 77 --------- .../RoundtripSpecs/TestExample.cs | 54 ++++++ src/Markdig/Roundtrip.md | 160 ++++++++++++++++++ 4 files changed, 215 insertions(+), 77 deletions(-) delete mode 100644 src/Markdig.Tests/Roundtrip.md create mode 100644 src/Markdig.Tests/RoundtripSpecs/TestExample.cs create mode 100644 src/Markdig/Roundtrip.md diff --git a/readme.md b/readme.md index 77ed95d95..8787d40a1 100644 --- a/readme.md +++ b/readme.md @@ -19,6 +19,7 @@ You can **try Markdig online** and compare it to other implementations on [babel - including **GFM fenced code blocks**. - **Extensible** architecture - Even the core Markdown/CommonMark parsing is pluggable, so it allows to disable builtin Markdown/Commonmark parsing (e.g [Disable HTML parsing](https://github.com/lunet-io/markdig/blob/7964bd0160d4c18e4155127a4c863d61ebd8944a/src/Markdig/MarkdownExtensions.cs#L306)) or change behaviour (e.g change matching `#` of a headers with `@`) +- [**Roundtrip support**](./src/Markdig/Roundtrip.md): Parses trivia (whitespace, newlines and other characters) to support lossless parse ⭢ render roundtrip. This enables changing markdown documents without introducing undesired trivia changes. - Built-in with **20+ extensions**, including: - 2 kind of tables: - [**Pipe tables**](src/Markdig.Tests/Specs/PipeTableSpecs.md) (inspired from GitHub tables and [PanDoc - Pipe Tables](http://pandoc.org/README.html#extension-pipe_tables)) diff --git a/src/Markdig.Tests/Roundtrip.md b/src/Markdig.Tests/Roundtrip.md deleted file mode 100644 index 401e7cc2f..000000000 --- a/src/Markdig.Tests/Roundtrip.md +++ /dev/null @@ -1,77 +0,0 @@ -# Roundtrip support -Roundtrip support allows parsing of Markdown to subsequently render it back to Markdown without changes. This requires storing all characters on the parse tree, including whitespace and special characters. This document outlines decisions and guidelines on how these characters are stored. - -# Guidelines -- newlines before blocks are assigned to that block -- whitespace starting on a line is assigned to the block on that line -- assigning whitespace *before* a node has precedence over asigning whitespace *after* a node -- whitespace vs trivia - - AtxHeading can have #s after the title, white are including as trivia - -## Quoteblock -Quoteblocks may have different syntactical characters applied per line. That is, some lines belonging to a Quoteblock may and others **may not** contain the quote marker character `>`. Each line of a Quoteblock therefore stores the quote marker character and its surrounding whitespace. - -## Lists -- beforewhitespace on list item - -## Trivia -- whitespace - - ` ` (space) - - `\t` - - `\f` - - `\v` -- trailing `#` -- TODO: ThematicBreak -- TODO: link url `>`, link title `(`, `'`, `"` - -# TODO -In order: -- ~~`p\n p`: affects many tests~~ -- ~~`\r\n` and `\r` support~~ -- ~~support SetextHeading~~ -- ~~support LinkReferenceDefinition~~ -- ~~support link parsing~~ -- ~~support AutolinkInline~~ -- ~~generate spec examples as tests for roundtrip~~ -- ~~check char.IsWhitespace() calls~~ -- ~~check char.IsNewline() calls~~ -- ~~introduce feature flag~~ -- ~~extract MarkdownRenderer~~ -- ~~cleanup NormalizeRenderer (MarkdownRenderer)~~ -- ~~deduplicate MarkdownRenderer and NormalizeRenderer code~~ -- ~~merge from main~~ -- ~~fix broken pre-existing tests~~ -- ~~use StringSlice where possible instead of String~~ -- ~~document newly added syntax properties~~ -- ~~review complete PR and follow conventions~~ -- do pull request feedback -- support extensions -- run perf test -- create todo list with perf optimization focus points -- optimize perf -- `\0` -- split HeadingBlock into AtxHeadingBlock and SetextHeadingBlock? -- document how trivia are handled generically and specifically -- write tree comparison tests? -- write tree visualization tool? - -# Pull request discussion -- LinkHelper duplication - - keep current? - - if not, how to deduplicate? -- StringSlice vs String - - StringSlice is preferred -- amount of tests - - should we create even more permutations using `\v`, `\f`? -- newlines - - Newline struct itself - - handling newlines - - should newlines be supported? - - LineBreakInline now also parses /r and /r/n: this effectively removes an optimization -- Example 207, 209, 291: Special-casing certain edgecases -- TrackTrivia flag trickling down into 5-6 classes - - InlineProcessor - - BlockProcessor - - MarkdownParser - - Markdown static class - - LinkReferenceDefinitionHelper diff --git a/src/Markdig.Tests/RoundtripSpecs/TestExample.cs b/src/Markdig.Tests/RoundtripSpecs/TestExample.cs new file mode 100644 index 000000000..6960a345f --- /dev/null +++ b/src/Markdig.Tests/RoundtripSpecs/TestExample.cs @@ -0,0 +1,54 @@ +using Markdig.Helpers; +using Markdig.Renderers.Roundtrip; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using NUnit.Framework; +using System.IO; + +namespace Markdig.Tests.RoundtripSpecs +{ + [TestFixture] + public class TestExample + { + [Test] + public void Test() + { + var markdown = $@" +# Test document +This document contains an unordered list. It uses tabs to indent. This test demonstrates +a method of making the input markdown uniform without altering any other markdown in the +resulting output file. + +- item1 + +>look, ma: +> my space is not normalized! +"; + MarkdownDocument markdownDocument = Markdown.Parse(markdown, trackTrivia: true); + var listBlock = markdownDocument[2] as ListBlock; + var listItem = listBlock[0] as ListItemBlock; + var paragraph = listItem[0] as ParagraphBlock; + var containerInline = new ContainerInline(); + containerInline.AppendChild(new LiteralInline(" my own text!")); + containerInline.AppendChild(new LineBreakInline { Newline = Newline.CarriageReturnLineFeed }); + paragraph.Inline = containerInline; + + var sw = new StringWriter(); + var rr = new RoundtripRenderer(sw); + rr.Write(markdownDocument); + var outputMarkdown = sw.ToString(); + var expected = $@" +# Test document +This document contains an unordered list. It uses tabs to indent. This test demonstrates +a method of making the input markdown uniform without altering any other markdown in the +resulting output file. + +- my own text! + +>look, ma: +> my space is not normalized! +"; + Assert.AreEqual(expected, outputMarkdown); + } + } +} diff --git a/src/Markdig/Roundtrip.md b/src/Markdig/Roundtrip.md new file mode 100644 index 000000000..4b9de0da3 --- /dev/null +++ b/src/Markdig/Roundtrip.md @@ -0,0 +1,160 @@ +# Roundtrip parser +Markdig supports parsing trivia characters and tracks the source position of these characters. This gives the ability to parse a document and then render a slightly changed document back. Without tracking trivia characters, the renderer must makes all kinds of assumptions on newlines, tabs, whitespace characters and other document details. + +To use this functionality, set the optional `trackTrivia` parameter to true when using the static `Markdown` class: +```csharp +MarkdownDocument markdownDocument = Markdown.Parse(inputMarkdown, trackTrivia = true); +MarkdownDocument markdownDocument = Markdown.Parse(inputMarkdown, pipeline, trackTrivia = true); +``` +You will get a parse tree where various `Trivia*` properties of `Block` and `Inline` instances now have instances. To write a document to Markdown using this tree, use the `RoundtripRenderer`: +```csharp +var sw = new StringWriter(); +var rr = new RoundtripRenderer(sw); +rr.Write(markdownDocument); +var outputMarkdown = sw.ToString(); +``` +You should expect the `outputMarkdown` to be equal to the `inputMarkdown`. + +# Demo test +For a simple test showcasing the feature, see the [`./Markdig.Tests/RoundtripSpecs/TestExample.cs`](../Markdig.Tests/RoundtripSpecs/TestExample.cs) file. + +# Trivia +Trivia are not specified by the CommonMark standard. As such any implementation decides for itself which tree nodes trivia are attached to. + +Trivia characters are: +- newlines: `\n`, `\r`, `\r\n` +- ` ` (space), `\f` (form feed), `\v` (vertical tab) +- unescaped string characters + +# Newlines +Blocks are almost always ended with a newline, therefore the `Block` class has it defined as property: +```csharp +/// +/// The last newline of this block +/// +public Newline Newline { get; set; } +``` +Consider a very simple valid Markdown document (for clarity's sake, the `\n` character is added): +```markdown +p\n +\n +p\n +``` +Above document consists of 5 characters, `p`, `\n`, `\n`, `p`, `\n` in sequence . Obviously, the two `p` characters are part of a separate paragraph block. The `\n` right next to each `p` is easy: we'll just attach it to either paragraph block as well. However, it is not clear what we should do with the middle `\n`: should it be attached to the first `p` or the second `p`? Let's look at a different example: + +```markdown +\n +p\n +\n +``` +Here, we only have *one* (paragraph)block, and thus must attach the first `\n` **and** last `\n` to that paragraph block. +The `Block` class therefore has `LinesBefore` and `LinesAfter` defined: +```csharp +/// +/// Gets or sets the empty lines occurring before this block. +/// Trivia: only parsed when is enabled, otherwise null. +/// +public List LinesBefore { get; set; } + +/// +/// Gets or sets the empty lines occurring after this block. +/// Trivia: only parsed when is enabled, otherwise null. +/// +public List LinesAfter { get; set; } +``` + +The choice to attach the middle `\n` from the first example is arbitrary. When parsing, it's easier and simpler to attach it to the first occuring block, so that's what Markdig does. + +**Rule: Newlines are attached to the first occurring node** + +The parse tree of the first example then becomes: +1. paragraph block `p` + - newline: `\n` + - after: `\n` +2. paragraph block `p` + - newline: `\n` + +In the second example, the parse tree is: +1. paragraph block `p` + - newline: `\n` + - before: `\n` + - after: `\n` + +Stated differently: Blocks *almost always have* a newline, *often* have *trivia after* and sometimes have *trivia before*. + +Keep in mind that paragraphs are a bit of a special case in Markdown. This is also the case with trivia parsing, where the `LineBreakInline` is considered part of the paragraph block, and not part of the trivia. Consider the following example: +```markdown +\n +text1\n +text2\n +\n +``` +The first `\n` is attached to the paragraph block as *trivia before*. The second `\n` is a LineBreakInline inline element, and not considered trivia. The third `\n` is the newline of the paragraph block. The fourth `\n` is attached as *trivia after*. + +# Trivia before and trivia after +All trivia in a document should be attached to a node. The `Block` class defines two properties to capture this: +```csharp +/// +/// Gets or sets the trivia right before this block. +/// Trivia: only parsed when is enabled, otherwise +/// . +/// +public StringSlice TriviaBefore { get; set; } + +/// +/// Gets or sets trivia occurring after this block. +/// Trivia: only parsed when is enabled, otherwise +/// . +/// +public StringSlice TriviaAfter { get; set; } +``` +Typically, this trivia occurs within the document before, in between or after blocks. Take these examples (the *interpunct*, aka *middle dot*: `·` is used to visualize a space character): +```markdown +·*·item1 +``` +```markdown +··*·item1 +``` +```markdown +·*··item1 +``` +All is valid markdown and define an unordered list with one paragraph block. The parse tree looks like this: +- ListBlock + - ListItemBlock + - Paragraph + - LiteralInline "item1" + +The parser assigns the trivia (spaces in above example) to the `ListItemBlock` and `ParagraphBlock` nodes respectively. + +# Enclosed trivia +Trivia may occur *within* nodes. In such case, a property is defined for each part of the syntax where trivia may occur. +Some inlines have escaped strings. These strings are set seperately on the parse tree of that inline. + +`LinkInline` and `FencedCodeBlock` are both examples where trivia is parsed within the node and the node contains properties for both escaped an unescaped strings. + +# Links and LinkReferences +Links and LinkReferences have a complex parsing implementation. The codebase currrently consists of a separate set of `Parse*Trivia` methods. These methods are duplicated from their source `Parse*` methods for simplicity's sake. Abstracting the trivia parsing in the source methods was considered, but that would make already complex parsing logic even more complex. Instead, the cost of maintaining a (mature) duplicated codebase was considered to be easier and less complex. + +# LinkReference +While LinkReferences are parsed, the `LinkReferenceDefinitionGroup` is not added to the document. The reason for this is to have the parse tree represent the input text as precise as possible. Adding the `LinkReferenceDefinitionGroup` would add a node not representing input text, and as such is omitted. + +# `/0` character +As per the [CommonMark 0.29 spec], the `/0` aka `U+0000` character is replaced with `/uFFFD`. Therefore, it is not - and never will be - possible to have exactly equal output Markdown as input, whenever there is a `/0` character in the input. + +**Rule: Exactly equal output Markdown given an input Markdown is only possible when the `/0` character is not present in the input Markdown** + +# NoBlocksFoundBlock +The spec states + +> Any sequence of characters is a valid CommonMark document. + +where + +> A character is a Unicode code point. Although some code points (for example, combining accents) do not correspond to characters in an intuitive sense, all code points count as characters for purposes of this spec. + +As such an input document containing trivia is, technically, also valid Markdown. To support rountrip parsing for documents that contain input characters - but these input characters do not resolve to any blocks, the `NoBlocksFoundBlock` is defined. + +**Rule: the `NoBlocksFoundBlock` is a Block representing a block of Markdown trivia where no other Block types are matched on** + +# Extensions +Extensions are currently not supported. If you're a writer or maintainer of an existing extension, would you be interested in writing a pull request to have your extension support roundtrip parsing? If you need any assistance, please reach out to @generateui. I'd be happy to help. \ No newline at end of file From 1d79ad8436bed57ba095aa8204cb29373778a53d Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Mon, 1 Mar 2021 23:11:31 +0100 Subject: [PATCH 112/120] rerun specFileGen --- src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs b/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs index 5a274f1a5..8406fdfa5 100644 --- a/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs +++ b/src/Markdig.Tests/RoundtripSpecs/CommonMark.generated.cs @@ -1,4 +1,3 @@ -// Generated: 2020-10-17 21:45:36 // -------------------------------- // Roundtrip From b6e38a0a96f3e6ce2e4e6db350d90e819ec214b3 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Mon, 1 Mar 2021 23:26:55 +0100 Subject: [PATCH 113/120] fix ContainerBlocksBlockQuotes_Example210 --- src/Markdig/Parsers/MarkdownParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index d48e110c0..e90435489 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -120,7 +120,7 @@ private void ProcessBlocks() if (TrackTrivia) { Block lastBlock = blockProcessor.LastBlock; - if (lastBlock == null) + if (lastBlock == null && document.Count == 0) { // this means we have unassigned characters var noBlocksFoundBlock = new NoBlocksFoundBlock(null); @@ -129,7 +129,7 @@ private void ProcessBlocks() noBlocksFoundBlock.LinesAfter.AddRange(linesBefore); document.Add(noBlocksFoundBlock); } - else if (blockProcessor.LinesBefore != null) + else if (lastBlock != null && blockProcessor.LinesBefore != null) { // this means we're out of lines, but still have unassigned empty lines. // thus, we'll assign the empty unsassigned lines to the last block From 80d4e6f344d981eac75bda9e970f35776a2b0551 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Wed, 3 Mar 2021 05:32:28 +0100 Subject: [PATCH 114/120] Fix StringSlice overlaps Co-authored-by: Miha Zupan --- src/Markdig/Helpers/StringSlice.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index aa1045dbf..6edca5481 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -507,15 +507,8 @@ public bool Overlaps(StringSlice other) { return false; } - if (Start <= other.End) - { - return true; - } - if (End <= other.Start) - { - return true; - } - return false; + + return Start <= other.End && End >= other.Start; } } -} \ No newline at end of file +} From 4bc2e847d526f078ac308fb9bd137a3b7327d44a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Thu, 4 Mar 2021 22:04:26 +0100 Subject: [PATCH 115/120] PR feedback --- src/Markdig/Helpers/CharHelper.cs | 6 --- src/Markdig/Helpers/LinkHelper.cs | 16 +------ src/Markdig/Helpers/Newline.cs | 45 +++++++++++++++++++ src/Markdig/Helpers/StringSlice.cs | 44 ------------------ src/Markdig/Parsers/MarkdownParser.cs | 2 +- .../Roundtrip/NoBlockFoundBlockRenderer.cs | 4 +- .../Renderers/Roundtrip/RoundtripRenderer.cs | 2 +- src/Markdig/Syntax/NoBlocksFoundBlock.cs | 4 +- 8 files changed, 52 insertions(+), 71 deletions(-) create mode 100644 src/Markdig/Helpers/Newline.cs diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index 532a118cd..d4af040bf 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -226,12 +226,6 @@ public static bool IsNewLineLineFeed(this char c) return c == '\n' || c == '\r'; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsNewLine(this char c) - { - return c == '\n' || c == '\r'; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsZero(this char c) { diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index fca1901a1..1a06a8d37 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -902,7 +902,7 @@ public static bool TryParseUrlTrivia(ref T text, out string link, out bool ha continue; } - if (c.IsNewLine()) + if (c.IsNewLineLineFeed()) { break; } @@ -1069,20 +1069,6 @@ public static bool IsValidDomain(string link, int prefixLength) segmentCount - lastUnderscoreSegment >= 2; // No underscores are present in the last two segments of the domain } - // apparently, below code is dead code (except for public api?) - //public static bool TryParseLinkReferenceDefinition(T text, out string label, out string url, - // out string title) where T : ICharIterator - //{ - // return TryParseLinkReferenceDefinition(ref text, out label, out url, out title); - //} - - //public static bool TryParseLinkReferenceDefinition(ref T text, out string label, out string url, out string title) - // where T : ICharIterator - //{ - // return TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out SourceSpan labelSpan, out SourceSpan urlSpan, - // out SourceSpan titleSpan); - //} - public static bool TryParseLinkReferenceDefinition(ref T text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan) where T : ICharIterator { url = null; diff --git a/src/Markdig/Helpers/Newline.cs b/src/Markdig/Helpers/Newline.cs new file mode 100644 index 000000000..ee1ff36f6 --- /dev/null +++ b/src/Markdig/Helpers/Newline.cs @@ -0,0 +1,45 @@ +using System; + +namespace Markdig.Helpers +{ + /// + /// Represents a character or set of characters that represent a separation + /// between two lines of text + /// + public enum Newline + { + None, + CarriageReturn, + LineFeed, + CarriageReturnLineFeed + } + + public static class NewlineExtensions + { + public static string AsString(this Newline newline) + { + if (newline == Newline.CarriageReturnLineFeed) + { + return "\r\n"; + } + if (newline == Newline.LineFeed) + { + return "\n"; + } + if (newline == Newline.CarriageReturn) + { + return "\r"; + } + return string.Empty; + } + + public static int Length(this Newline newline) => newline switch + { + Newline.None => 0, + Newline.CarriageReturn => 1, + Newline.LineFeed => 1, + Newline.CarriageReturnLineFeed => 2, + _ => throw new NotSupportedException(), + }; + } +} diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index aa1045dbf..7e89b6d2d 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -7,50 +7,6 @@ namespace Markdig.Helpers { - /// - /// Wrap newline so we have type-safety and static accessibility - /// - public enum Newline - { - None, - CarriageReturn, - LineFeed, - CarriageReturnLineFeed - } - - public static class NewlineExtensions - { - public static string AsString(this Newline newline) - { - if (newline == Newline.CarriageReturnLineFeed) - { - return "\r\n"; - } - if (newline == Newline.LineFeed) - { - return "\n"; - } - if (newline == Newline.CarriageReturn) - { - return "\r"; - } - return string.Empty; - } - public static int Length(this Newline newline) => newline switch - { - Newline.None => 0, - Newline.CarriageReturn => 1, - Newline.LineFeed => 1, - Newline.CarriageReturnLineFeed => 2, - _ => throw new NotSupportedException(), - }; - } - // public static implicit operator string (Newline newline) - // { - // } - // public int Length => - //} - /// /// A lightweight struct that represents a slice of a string. /// diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index e90435489..7ee64d696 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -123,7 +123,7 @@ private void ProcessBlocks() if (lastBlock == null && document.Count == 0) { // this means we have unassigned characters - var noBlocksFoundBlock = new NoBlocksFoundBlock(null); + var noBlocksFoundBlock = new EmptyBlock (null); List linesBefore = blockProcessor.UseLinesBefore(); noBlocksFoundBlock.LinesAfter = new List(); noBlocksFoundBlock.LinesAfter.AddRange(linesBefore); diff --git a/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs index 09c0c7ee4..708f22cd1 100644 --- a/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/NoBlockFoundBlockRenderer.cs @@ -2,9 +2,9 @@ namespace Markdig.Renderers.Roundtrip { - public class NoBlockFoundBlockRenderer : RoundtripObjectRenderer + public class EmptyBlockRenderer : RoundtripObjectRenderer { - protected override void Write(RoundtripRenderer renderer, NoBlocksFoundBlock noBlocksFoundBlock) + protected override void Write(RoundtripRenderer renderer, EmptyBlock noBlocksFoundBlock) { renderer.RenderLinesAfter(noBlocksFoundBlock); } diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs index a2bb5c60e..4f0307664 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -31,7 +31,7 @@ public RoundtripRenderer(TextWriter writer) : base(writer) ObjectRenderers.Add(new ThematicBreakRenderer()); ObjectRenderers.Add(new LinkReferenceDefinitionGroupRenderer()); ObjectRenderers.Add(new LinkReferenceDefinitionRenderer()); - ObjectRenderers.Add(new NoBlockFoundBlockRenderer()); + ObjectRenderers.Add(new EmptyBlockRenderer()); // Default inline renderers ObjectRenderers.Add(new AutolinkInlineRenderer()); diff --git a/src/Markdig/Syntax/NoBlocksFoundBlock.cs b/src/Markdig/Syntax/NoBlocksFoundBlock.cs index 50528d8c5..57d255b3a 100644 --- a/src/Markdig/Syntax/NoBlocksFoundBlock.cs +++ b/src/Markdig/Syntax/NoBlocksFoundBlock.cs @@ -6,9 +6,9 @@ namespace Markdig.Syntax /// Block representing a document with characters but no blocks. This can /// happen when an input document consists solely of trivia. /// - public sealed class NoBlocksFoundBlock : LeafBlock + public sealed class EmptyBlock : LeafBlock { - public NoBlocksFoundBlock(BlockParser parser) : base(parser) + public EmptyBlock (BlockParser parser) : base(parser) { } } From 9031be96f87fc00bb8c3e2138c3dae9f99df3a82 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Thu, 4 Mar 2021 22:35:48 +0100 Subject: [PATCH 116/120] rename Newline to NewLine --- .../RoundtripSpecs/TestExample.cs | 2 +- src/Markdig.Tests/TestStringSliceList.cs | 18 ++++++------ .../CustomContainers/CustomContainer.cs | 2 +- src/Markdig/Helpers/LineReader.cs | 6 ++-- src/Markdig/Helpers/LinkHelper.cs | 12 ++++---- src/Markdig/Helpers/Newline.cs | 23 +++++++-------- src/Markdig/Helpers/StringLine.cs | 12 ++++---- src/Markdig/Helpers/StringLineGroup.cs | 28 +++++++++---------- src/Markdig/Helpers/StringSlice.cs | 14 +++++----- src/Markdig/Parsers/BlockProcessor.cs | 6 ++-- src/Markdig/Parsers/FencedBlockParserBase.cs | 4 +-- src/Markdig/Parsers/FencedCodeBlockParser.cs | 2 +- src/Markdig/Parsers/HeadingBlockParser.cs | 2 +- src/Markdig/Parsers/HtmlBlockParser.cs | 4 +-- .../Parsers/IndentedCodeBlockParser.cs | 6 ++-- src/Markdig/Parsers/InlineProcessor.cs | 4 +-- .../Parsers/Inlines/EscapeInlineParser.cs | 6 ++-- .../Parsers/Inlines/LineBreakInlineParser.cs | 12 ++++---- src/Markdig/Parsers/ListBlockParser.cs | 4 +-- src/Markdig/Parsers/ParagraphBlockParser.cs | 8 +++--- src/Markdig/Parsers/QuoteBlockParser.cs | 6 ++-- src/Markdig/Parsers/ThematicBreakParser.cs | 4 +-- .../Renderers/Roundtrip/CodeBlockRenderer.cs | 6 ++-- .../Renderers/Roundtrip/HeadingRenderer.cs | 4 +-- .../Inlines/LineBreakInlineRenderer.cs | 2 +- .../LinkReferenceDefinitionRenderer.cs | 2 +- .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 4 +-- .../Renderers/Roundtrip/RoundtripRenderer.cs | 6 ++-- .../Roundtrip/ThematicBreakRenderer.cs | 2 +- src/Markdig/Renderers/TextRendererBase.cs | 4 +-- src/Markdig/Syntax/Block.cs | 2 +- src/Markdig/Syntax/CharIteratorHelper.cs | 16 +++++------ src/Markdig/Syntax/FencedCodeBlock.cs | 2 +- src/Markdig/Syntax/HeadingBlock.cs | 2 +- src/Markdig/Syntax/IFencedBlock.cs | 6 ++-- src/Markdig/Syntax/Inlines/LineBreakInline.cs | 2 +- src/Markdig/Syntax/LeafBlock.cs | 4 +-- src/Markdig/Syntax/LinkReferenceDefinition.cs | 4 +-- src/Markdig/Syntax/QuoteBlock.cs | 2 +- 39 files changed, 128 insertions(+), 127 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/TestExample.cs b/src/Markdig.Tests/RoundtripSpecs/TestExample.cs index 6960a345f..3de46d8b1 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestExample.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestExample.cs @@ -30,7 +30,7 @@ resulting output file. var paragraph = listItem[0] as ParagraphBlock; var containerInline = new ContainerInline(); containerInline.AppendChild(new LiteralInline(" my own text!")); - containerInline.AppendChild(new LineBreakInline { Newline = Newline.CarriageReturnLineFeed }); + containerInline.AppendChild(new LineBreakInline { NewLine = NewLine.CarriageReturnLineFeed }); paragraph.Inline = containerInline; var sw = new StringWriter(); diff --git a/src/Markdig.Tests/TestStringSliceList.cs b/src/Markdig.Tests/TestStringSliceList.cs index 680efe4bc..cc833ab27 100644 --- a/src/Markdig.Tests/TestStringSliceList.cs +++ b/src/Markdig.Tests/TestStringSliceList.cs @@ -16,8 +16,8 @@ public void TestStringLineGroupSimple() { var text = new StringLineGroup(4) { - new StringSlice("ABC", Newline.LineFeed), - new StringSlice("E", Newline.LineFeed), + new StringSlice("ABC", NewLine.LineFeed), + new StringSlice("E", NewLine.LineFeed), new StringSlice("F") }; @@ -35,8 +35,8 @@ public void TestStringLineGroupWithSlices() { var text = new StringLineGroup(4) { - new StringSlice("XABC", Newline.LineFeed) { Start = 1}, - new StringSlice("YYE", Newline.LineFeed) { Start = 2}, + new StringSlice("XABC", NewLine.LineFeed) { Start = 1}, + new StringSlice("YYE", NewLine.LineFeed) { Start = 2}, new StringSlice("ZZZF") { Start = 3 } }; @@ -61,7 +61,7 @@ public void TestStringLineGroupSaveAndRestore() { var text = new StringLineGroup(4) { - new StringSlice("ABCD", Newline.LineFeed), + new StringSlice("ABCD", NewLine.LineFeed), new StringSlice("EF"), }.ToCharIterator(); @@ -99,7 +99,7 @@ public void TestSkipWhitespaces() [Test] public void TestStringLineGroupWithModifiedStart() { - var line1 = new StringSlice(" ABC", Newline.LineFeed); + var line1 = new StringSlice(" ABC", NewLine.LineFeed); line1.NextChar(); line1.NextChar(); @@ -115,7 +115,7 @@ public void TestStringLineGroupWithModifiedStart() [Test] public void TestStringLineGroupWithTrim() { - var line1 = new StringSlice(" ABC ", Newline.LineFeed); + var line1 = new StringSlice(" ABC ", NewLine.LineFeed); line1.NextChar(); line1.NextChar(); @@ -133,8 +133,8 @@ public void TestStringLineGroupIteratorPeekChar() { var iterator = new StringLineGroup(4) { - new StringSlice("ABC", Newline.LineFeed), - new StringSlice("E", Newline.LineFeed), + new StringSlice("ABC", NewLine.LineFeed), + new StringSlice("E", NewLine.LineFeed), new StringSlice("F") }.ToCharIterator(); diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs index 2a61694e4..71b3e5f11 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainer.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainer.cs @@ -51,7 +51,7 @@ public CustomContainer(BlockParser parser) : base(parser) public StringSlice TriviaAfterArguments { get; set; } /// - public Newline InfoNewline { get; set; } + public NewLine InfoNewLine { get; set; } /// public StringSlice TriviaBeforeClosingFence { get; set; } diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index 3996edfa9..a1196a526 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -48,12 +48,12 @@ public StringSlice ReadLine() if (c == '\r') { int length = 1; - var newline = Newline.CarriageReturn; + var newline = NewLine.CarriageReturn; if (c == '\r' && (uint)(i + 1) < (uint)text.Length && text[i + 1] == '\n') { i++; length = 2; - newline = Newline.CarriageReturnLineFeed; + newline = NewLine.CarriageReturnLineFeed; } var slice = new StringSlice(text, sourcePosition, i - length, newline); @@ -63,7 +63,7 @@ public StringSlice ReadLine() if (c == '\n') { - var slice = new StringSlice(text, sourcePosition, i - 1, Newline.LineFeed); + var slice = new StringSlice(text, sourcePosition, i - 1, NewLine.LineFeed); SourcePosition = i + 1; return slice; } diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 1a06a8d37..3679d7059 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -1168,7 +1168,7 @@ public static bool TryParseLinkReferenceDefinitionTrivia( out string title, // can contain non-consecutive newlines out SourceSpan unescapedTitle, out char titleEnclosingCharacter, - out Newline newline, + out NewLine newLine, out SourceSpan triviaAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, @@ -1181,7 +1181,7 @@ public static bool TryParseLinkReferenceDefinitionTrivia( triviaBeforeTitle = SourceSpan.Empty; title = null; unescapedTitle = SourceSpan.Empty; - newline = Newline.None; + newLine = NewLine.None; urlSpan = SourceSpan.Empty; titleSpan = SourceSpan.Empty; @@ -1223,7 +1223,7 @@ public static bool TryParseLinkReferenceDefinitionTrivia( int triviaBeforeTitleStart = text.Start; var saved = text; - var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out newline); + var hasWhiteSpaces = CharIteratorHelper.TrimStartAndCountNewLines(ref text, out int newLineCount, out newLine); triviaBeforeTitle = new SourceSpan(triviaBeforeTitleStart, text.Start - 1); var c = text.CurrentChar; @@ -1288,15 +1288,15 @@ public static bool TryParseLinkReferenceDefinitionTrivia( { if (c == '\n') { - newline = Newline.LineFeed; + newLine = NewLine.LineFeed; } else if (c == '\r' && text.PeekChar() == '\n') { - newline = Newline.CarriageReturnLineFeed; + newLine = NewLine.CarriageReturnLineFeed; } else if (c == '\r') { - newline = Newline.CarriageReturn; + newLine = NewLine.CarriageReturn; } } diff --git a/src/Markdig/Helpers/Newline.cs b/src/Markdig/Helpers/Newline.cs index ee1ff36f6..a87bb2c7b 100644 --- a/src/Markdig/Helpers/Newline.cs +++ b/src/Markdig/Helpers/Newline.cs @@ -6,7 +6,7 @@ namespace Markdig.Helpers /// Represents a character or set of characters that represent a separation /// between two lines of text /// - public enum Newline + public enum NewLine { None, CarriageReturn, @@ -14,32 +14,33 @@ public enum Newline CarriageReturnLineFeed } - public static class NewlineExtensions + public static class NewLineExtensions { - public static string AsString(this Newline newline) + public static string AsString(this NewLine newLine) { - if (newline == Newline.CarriageReturnLineFeed) + if (newLine == NewLine.CarriageReturnLineFeed) { return "\r\n"; } - if (newline == Newline.LineFeed) + if (newLine == NewLine.LineFeed) { return "\n"; } - if (newline == Newline.CarriageReturn) + if (newLine == NewLine.CarriageReturn) { return "\r"; } return string.Empty; } - public static int Length(this Newline newline) => newline switch + public static int Length(this NewLine newline) => newline switch { - Newline.None => 0, - Newline.CarriageReturn => 1, - Newline.LineFeed => 1, - Newline.CarriageReturnLineFeed => 2, + NewLine.None => 0, + NewLine.CarriageReturn => 1, + NewLine.LineFeed => 1, + NewLine.CarriageReturnLineFeed => 2, _ => throw new NotSupportedException(), }; } } + diff --git a/src/Markdig/Helpers/StringLine.cs b/src/Markdig/Helpers/StringLine.cs index 2c37a0a74..25431d76b 100644 --- a/src/Markdig/Helpers/StringLine.cs +++ b/src/Markdig/Helpers/StringLine.cs @@ -16,7 +16,7 @@ public struct StringLine public StringLine(ref StringSlice slice) : this() { Slice = slice; - Newline = slice.Newline; + NewLine = slice.NewLine; } /// @@ -26,13 +26,13 @@ public StringLine(ref StringSlice slice) : this() /// The line. /// The column. /// The position. - public StringLine(StringSlice slice, int line, int column, int position, Newline newline) + public StringLine(StringSlice slice, int line, int column, int position, NewLine newLine) { Slice = slice; Line = line; Column = column; Position = position; - Newline = newline; + NewLine = newLine; } /// @@ -42,13 +42,13 @@ public StringLine(StringSlice slice, int line, int column, int position, Newline /// The line. /// The column. /// The position. - public StringLine(ref StringSlice slice, int line, int column, int position, Newline newline) + public StringLine(ref StringSlice slice, int line, int column, int position, NewLine newLine) { Slice = slice; Line = line; Column = column; Position = position; - Newline = newline; + NewLine = newLine; } /// @@ -74,7 +74,7 @@ public StringLine(ref StringSlice slice, int line, int column, int position, New /// /// The newline. /// - public Newline Newline; + public NewLine NewLine; /// /// Performs an implicit conversion from to . diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index ad795357d..6f53f75e9 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -140,12 +140,12 @@ public readonly StringSlice ToSlice(List lineOffsets = null) // Else use a builder var builder = StringBuilderCache.Local(); int previousStartOfLine = 0; - var newline = Newline.None; + var newLine = NewLine.None; for (int i = 0; i < Count; i++) { if (i > 0) { - builder.Append(newline.AsString()); + builder.Append(newLine.AsString()); previousStartOfLine = builder.Length; } ref var line = ref Lines[i]; @@ -153,7 +153,7 @@ public readonly StringSlice ToSlice(List lineOffsets = null) { builder.Append(line.Slice.Text, line.Slice.Start, line.Slice.Length); } - newline = line.Newline; + newLine = line.NewLine; lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, previousStartOfLine, builder.Length)); } @@ -225,7 +225,7 @@ public Iterator(StringLineGroup lines) for (int i = 0; i < lines.Count; i++) { var line = lines.Lines[i]; - End += line.Slice.Length + line.Newline.Length(); // Add chars + End += line.Slice.Length + line.NewLine.Length(); // Add chars } NextChar(); } @@ -277,29 +277,29 @@ public char NextChar() } else { - var newline = slice.Newline; + var newLine = slice.NewLine; if (_offset == slice.Length) { - if (newline == Newline.LineFeed) + if (newLine == NewLine.LineFeed) { CurrentChar = '\n'; SliceIndex++; _offset = -1; } - else if (newline == Newline.CarriageReturn) + else if (newLine == NewLine.CarriageReturn) { CurrentChar = '\r'; SliceIndex++; _offset = -1; } - else if (newline == Newline.CarriageReturnLineFeed) + else if (newLine == NewLine.CarriageReturnLineFeed) { CurrentChar = '\r'; } } else if (_offset + 1 == slice.Length) { - if (newline == Newline.CarriageReturnLineFeed) + if (newLine == NewLine.CarriageReturnLineFeed) { CurrentChar = '\n'; SliceIndex++; @@ -331,7 +331,7 @@ public readonly char PeekChar(int offset = 1) int sliceIndex = SliceIndex; var line = _lines.Lines[sliceIndex]; var slice = line.Slice; - if (!(line.Newline == Newline.CarriageReturnLineFeed && offset + 1 == slice.Length)) + if (!(line.NewLine == NewLine.CarriageReturnLineFeed && offset + 1 == slice.Length)) { while (offset > slice.Length) { @@ -344,7 +344,7 @@ public readonly char PeekChar(int offset = 1) } else { - if (slice.Newline == Newline.CarriageReturnLineFeed) + if (slice.NewLine == NewLine.CarriageReturnLineFeed) { return '\n'; // /n of /r/n (second character) } @@ -352,15 +352,15 @@ public readonly char PeekChar(int offset = 1) if (offset == slice.Length) { - if (line.Newline == Newline.LineFeed) + if (line.NewLine == NewLine.LineFeed) { return '\n'; } - if (line.Newline == Newline.CarriageReturn) + if (line.NewLine == NewLine.CarriageReturn) { return '\r'; } - if (line.Newline == Newline.CarriageReturnLineFeed) + if (line.NewLine == NewLine.CarriageReturnLineFeed) { return '\r'; // /r of /r/n (first character) } diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 7e89b6d2d..5e071d9d9 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -27,19 +27,19 @@ public StringSlice(string text) Text = text; Start = 0; End = (Text?.Length ?? 0) - 1; - Newline = Newline.None; + NewLine = NewLine.None; } /// /// Initializes a new instance of the struct. /// /// The text. - public StringSlice(string text, Newline newline) + public StringSlice(string text, NewLine newline) { Text = text; Start = 0; End = (Text?.Length ?? 0) - 1; - Newline = newline; + NewLine = newline; } /// @@ -57,7 +57,7 @@ public StringSlice(string text, int start, int end) Text = text; Start = start; End = end; - Newline = Newline.None; + NewLine = NewLine.None; } /// @@ -67,7 +67,7 @@ public StringSlice(string text, int start, int end) /// The start. /// The end. /// - public StringSlice(string text, int start, int end, Newline newline) + public StringSlice(string text, int start, int end, NewLine newline) { if (text is null) ThrowHelper.ArgumentNullException_text(); @@ -75,7 +75,7 @@ public StringSlice(string text, int start, int end, Newline newline) Text = text; Start = start; End = end; - Newline = newline; + NewLine = newline; } /// @@ -98,7 +98,7 @@ public StringSlice(string text, int start, int end, Newline newline) /// public readonly int Length => End - Start + 1; - public Newline Newline; + public NewLine NewLine; /// /// Gets the current character. diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 47ca0e674..c56b48c9e 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -202,7 +202,7 @@ internal List UseLinesBefore() /// /// Gets or sets the stack of empty lines not yet assigned to any . /// An entry may contain an empty . In that case the - /// is relevant. Otherwise, the + /// is relevant. Otherwise, the /// entry will contain trivia. /// public List LinesBefore { get; set; } @@ -740,7 +740,7 @@ private void TryContinueBlocks() if (TrackTrivia) { LinesBefore ??= new List(); - var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.Newline); + var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.NewLine); LinesBefore.Add(line); Line.Start = StartBeforeIndent; } @@ -820,7 +820,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) if (TrackTrivia) { LinesBefore ??= new List(); - var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.Newline); + var line = new StringSlice(Line.Text, TriviaStart, Line.Start - 1, Line.NewLine); LinesBefore.Add(line); Line.Start = StartBeforeIndent; } diff --git a/src/Markdig/Parsers/FencedBlockParserBase.cs b/src/Markdig/Parsers/FencedBlockParserBase.cs index fe2ec54fc..bc58ca943 100644 --- a/src/Markdig/Parsers/FencedBlockParserBase.cs +++ b/src/Markdig/Parsers/FencedBlockParserBase.cs @@ -166,7 +166,7 @@ public static bool RoundtripInfoParser(BlockProcessor blockProcessor, ref String fenced.Arguments = HtmlHelper.Unescape(arg.ToString()); fenced.UnescapedArguments = arg; fenced.TriviaAfterArguments = afterArg; - fenced.InfoNewline = line.Newline; + fenced.InfoNewLine = line.NewLine; return true; } @@ -331,7 +331,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) var fencedBlock = block as IFencedBlock; fencedBlock.ClosingFencedCharCount = closingCount; - fencedBlock.Newline = processor.Line.Newline; + fencedBlock.NewLine = processor.Line.NewLine; fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1); fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim); diff --git a/src/Markdig/Parsers/FencedCodeBlockParser.cs b/src/Markdig/Parsers/FencedCodeBlockParser.cs index a24fef78f..d53364b93 100644 --- a/src/Markdig/Parsers/FencedCodeBlockParser.cs +++ b/src/Markdig/Parsers/FencedCodeBlockParser.cs @@ -32,7 +32,7 @@ protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor) IndentCount = processor.Indent, LinesBefore = processor.UseLinesBefore(), TriviaBefore = processor.UseTrivia(processor.Start - 1), - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }; } diff --git a/src/Markdig/Parsers/HeadingBlockParser.cs b/src/Markdig/Parsers/HeadingBlockParser.cs index 983251f73..d283868ac 100644 --- a/src/Markdig/Parsers/HeadingBlockParser.cs +++ b/src/Markdig/Parsers/HeadingBlockParser.cs @@ -88,7 +88,7 @@ public override BlockState TryOpen(BlockProcessor processor) Span = { Start = sourcePosition }, TriviaBefore = processor.UseTrivia(sourcePosition - 1), LinesBefore = processor.UseLinesBefore(), - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }; processor.NewBlocks.Push(headingBlock); if (!processor.TrackTrivia) diff --git a/src/Markdig/Parsers/HtmlBlockParser.cs b/src/Markdig/Parsers/HtmlBlockParser.cs index c515eb515..1ac6822eb 100644 --- a/src/Markdig/Parsers/HtmlBlockParser.cs +++ b/src/Markdig/Parsers/HtmlBlockParser.cs @@ -263,7 +263,7 @@ private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock) if (result != BlockState.BreakDiscard) { htmlBlock.Span.End = line.End; - htmlBlock.Newline = state.Line.Newline; + htmlBlock.NewLine = state.Line.NewLine; } return result; @@ -279,7 +279,7 @@ private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int Span = new SourceSpan(startPosition, startPosition + state.Line.End), //BeforeWhitespace = state.PopBeforeWhitespace(startPosition - 1), LinesBefore = state.UseLinesBefore(), - Newline = state.Line.Newline, + NewLine = state.Line.NewLine, }); return BlockState.Continue; } diff --git a/src/Markdig/Parsers/IndentedCodeBlockParser.cs b/src/Markdig/Parsers/IndentedCodeBlockParser.cs index ea7210026..38cd28d1a 100644 --- a/src/Markdig/Parsers/IndentedCodeBlockParser.cs +++ b/src/Markdig/Parsers/IndentedCodeBlockParser.cs @@ -37,7 +37,7 @@ public override BlockState TryOpen(BlockProcessor processor) Column = processor.Column, Span = new SourceSpan(processor.Start, processor.Line.End), LinesBefore = processor.UseLinesBefore(), - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }; var codeBlockLine = new CodeBlockLine { @@ -98,7 +98,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) }; cb.CodeBlockLines ??= new List(); cb.CodeBlockLines.Add(codeBlockLine); - cb.Newline = processor.Line.Newline; // ensure block newline is last newline + cb.NewLine = processor.Line.NewLine; // ensure block newline is last newline } return BlockState.Continue; @@ -126,7 +126,7 @@ public override bool Close(BlockProcessor processor, Block block) if (processor.TrackTrivia) { var quoteLine = codeBlock.CodeBlockLines[i]; - var emptyLine = new StringSlice(line.Slice.Text, quoteLine.TriviaBefore.Start, line.Slice.End, line.Newline); + var emptyLine = new StringSlice(line.Slice.Text, quoteLine.TriviaBefore.Start, line.Slice.End, line.NewLine); block.LinesAfter ??= new List(); block.LinesAfter.Add(emptyLine); } diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 795697e08..676e9c95b 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -282,8 +282,8 @@ public void ProcessInlineLeaf(LeafBlock leafBlock) { if (!(leafBlock is HeadingBlock)) { - var newline = leafBlock.Newline; - leafBlock.Inline.AppendChild(new LineBreakInline { Newline = newline }); + var newLine = leafBlock.NewLine; + leafBlock.Inline.AppendChild(new LineBreakInline { NewLine = newLine }); } } diff --git a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs index a9905c207..aa9aae303 100644 --- a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs @@ -45,10 +45,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { if (c == '\n' || c == '\r') { - var newline = c == '\n' ? Newline.LineFeed : Newline.CarriageReturn; + var newline = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn; if (c == '\r' && slice.PeekChar() == '\n') { - newline = Newline.CarriageReturnLineFeed; + newline = NewLine.CarriageReturnLineFeed; } processor.Inline = new LineBreakInline() { @@ -57,7 +57,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, Line = line, Column = column, - Newline = newline + NewLine = newline }; processor.Inline.Span.End = processor.Inline.Span.Start + 1; slice.NextChar(); diff --git a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs index d7062e5d0..123d65b97 100644 --- a/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LineBreakInlineParser.cs @@ -37,24 +37,24 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var startPosition = slice.Start; var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace(); - var newline = Newline.LineFeed; + var newLine = NewLine.LineFeed; if (processor.TrackTrivia) { if (slice.CurrentChar == '\r') { if (slice.PeekChar() == '\n') { - newline = Newline.CarriageReturnLineFeed; + newLine = NewLine.CarriageReturnLineFeed; slice.NextChar(); // Skip \n } else { - newline = Newline.CarriageReturn; + newLine = NewLine.CarriageReturn; } } else { - newline = Newline.LineFeed; + newLine = NewLine.LineFeed; } } else @@ -72,9 +72,9 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) IsHard = EnableSoftAsHard || (slice.Start != 0 && hasDoubleSpacesBefore), Line = line, Column = column, - Newline = newline + NewLine = newLine }; - processor.Inline.Span.End = processor.Inline.Span.Start + (newline == Newline.CarriageReturnLineFeed ? 1 : 0); + processor.Inline.Span.End = processor.Inline.Span.Start + (newLine == NewLine.CarriageReturnLineFeed ? 1 : 0); return true; } } diff --git a/src/Markdig/Parsers/ListBlockParser.cs b/src/Markdig/Parsers/ListBlockParser.cs index 78732409c..94f36542c 100644 --- a/src/Markdig/Parsers/ListBlockParser.cs +++ b/src/Markdig/Parsers/ListBlockParser.cs @@ -171,7 +171,7 @@ private BlockState TryContinueListItem(BlockProcessor state, ListItemBlock listI // Update list-item source end position listItem.UpdateSpanEnd(state.Line.End); - listItem.Newline = state.Line.Newline; + listItem.NewLine = state.Line.NewLine; return BlockState.Continue; } @@ -280,7 +280,7 @@ private BlockState TryParseListItem(BlockProcessor state, Block block) TriviaBefore = triviaBefore, Span = new SourceSpan(sourcePosition, sourceEndPosition), LinesBefore = state.UseLinesBefore(), - Newline = state.Line.Newline, + NewLine = state.Line.NewLine, }; state.NewBlocks.Push(newListItem); diff --git a/src/Markdig/Parsers/ParagraphBlockParser.cs b/src/Markdig/Parsers/ParagraphBlockParser.cs index 2615485b4..e0a24ff4f 100644 --- a/src/Markdig/Parsers/ParagraphBlockParser.cs +++ b/src/Markdig/Parsers/ParagraphBlockParser.cs @@ -29,7 +29,7 @@ public override BlockState TryOpen(BlockProcessor processor) Column = processor.Column, Span = new SourceSpan(processor.Line.Start, processor.Line.End), LinesBefore = processor.UseLinesBefore(), - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }); return BlockState.Continue; } @@ -45,7 +45,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) { return TryParseSetexHeading(processor, block); } - block.Newline = processor.Line.Newline; + block.NewLine = processor.Line.NewLine; block.UpdateSpanEnd(processor.Line.End); return BlockState.Continue; } @@ -132,10 +132,10 @@ private BlockState TryParseSetexHeading(BlockProcessor state, Block block) TriviaBefore = state.UseTrivia(sourcePosition - 1), // remove dashes TriviaAfter = new StringSlice(state.Line.Text, state.Start, line.End), LinesBefore = paragraph.LinesBefore, - Newline = state.Line.Newline, + NewLine = state.Line.NewLine, IsSetext = true, HeaderCharCount = count, - SetextNewline = paragraph.Newline, + SetextNewline = paragraph.NewLine, }; if (!state.TrackTrivia) { diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index eedfefb99..c1e8410f8 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -72,7 +72,7 @@ public override BlockState TryOpen(BlockProcessor processor) TriviaAfter = triviaAfter, QuoteChar = true, HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }); processor.NewBlocks.Push(quoteBlock); if (!wasEmptyLine) @@ -107,7 +107,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) quote.QuoteLines.Add(new QuoteBlockLine { QuoteChar = false, - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }); return BlockState.None; } @@ -139,7 +139,7 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar, TriviaBefore = TriviaSpaceBefore, TriviaAfter = triviaAfter, - Newline = processor.Line.Newline, + NewLine = processor.Line.NewLine, }); if (!wasEmptyLine) diff --git a/src/Markdig/Parsers/ThematicBreakParser.cs b/src/Markdig/Parsers/ThematicBreakParser.cs index 5e99e87fd..2ba9b4798 100644 --- a/src/Markdig/Parsers/ThematicBreakParser.cs +++ b/src/Markdig/Parsers/ThematicBreakParser.cs @@ -95,8 +95,8 @@ public override BlockState TryOpen(BlockProcessor processor) //BeforeWhitespace = beforeWhitespace, //AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition), LinesBefore = processor.UseLinesBefore(), - Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.Newline), //include whitespace for now - Newline = processor.Line.Newline, + Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.NewLine), //include whitespace for now + NewLine = processor.Line.NewLine, }); return BlockState.BreakDiscard; } diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index 1e24f9abc..bbea06df2 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -53,7 +53,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) renderer.Write(attributes); } */ - renderer.WriteLine(fencedCodeBlock.InfoNewline); + renderer.WriteLine(fencedCodeBlock.InfoNewLine); renderer.WriteLeafRawLines(obj); @@ -63,7 +63,7 @@ protected override void Write(RoundtripRenderer renderer, CodeBlock obj) if (!string.IsNullOrEmpty(closing)) { // See example 207: "> ```\nfoo\n```" - renderer.WriteLine(obj.Newline); + renderer.WriteLine(obj.NewLine); } renderer.Write(obj.TriviaAfter); } @@ -94,7 +94,7 @@ public void WriteLeafRawLines(RoundtripRenderer renderer, LeafBlock leafBlock) { var slice = slices[i].Slice; renderer.Write(ref slices[i].Slice); - renderer.WriteLine(slice.Newline); + renderer.WriteLine(slice.NewLine); } } } diff --git a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs index 9fae3fb55..2cd9f468d 100644 --- a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs @@ -34,7 +34,7 @@ protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) renderer.WriteLine(obj.SetextNewline); renderer.Write(obj.TriviaBefore); renderer.Write(line); - renderer.WriteLine(obj.Newline); + renderer.WriteLine(obj.NewLine); renderer.Write(obj.TriviaAfter); renderer.RenderLinesAfter(obj); @@ -52,7 +52,7 @@ protected override void Write(RoundtripRenderer renderer, HeadingBlock obj) renderer.Write(obj.TriviaAfterAtxHeaderChar); renderer.WriteLeafInline(obj); renderer.Write(obj.TriviaAfter); - renderer.WriteLine(obj.Newline); + renderer.WriteLine(obj.NewLine); renderer.RenderLinesAfter(obj); } diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs index a6bdab6d2..7edcd6574 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs @@ -24,7 +24,7 @@ protected override void Write(RoundtripRenderer renderer, LineBreakInline obj) renderer.Write("\\"); //renderer.Write(obj.IsBackslash ? "\\" : " "); } - renderer.WriteLine(obj.Newline); + renderer.WriteLine(obj.NewLine); } } } \ No newline at end of file diff --git a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs index 43704a6c0..9e7c26931 100644 --- a/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/LinkReferenceDefinitionRenderer.cs @@ -43,7 +43,7 @@ protected override void Write(RoundtripRenderer renderer, LinkReferenceDefinitio renderer.Write(close); } renderer.Write(linkDef.TriviaAfter); - renderer.Write(linkDef.Newline.AsString()); + renderer.Write(linkDef.NewLine.AsString()); renderer.RenderLinesAfter(linkDef); } diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index fda66725a..926a31274 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -39,11 +39,11 @@ protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) { var emptyLeafBlock = new ParagraphBlock { - Newline = quoteLine.Newline + NewLine = quoteLine.NewLine }; var newline = new LineBreakInline { - Newline = quoteLine.Newline + NewLine = quoteLine.NewLine }; var container = new ContainerInline(); container.AppendChild(newline); diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs index 4f0307664..2e0c7d638 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -63,7 +63,7 @@ public void WriteLeafRawLines(LeafBlock leafBlock) { var slice = slices[i].Slice; Write(ref slice); - WriteLine(slice.Newline); + WriteLine(slice.NewLine); } } } @@ -77,7 +77,7 @@ public void RenderLinesBefore(Block block) foreach (var line in block.LinesBefore) { Write(line); - WriteLine(line.Newline); + WriteLine(line.NewLine); } } @@ -91,7 +91,7 @@ public void RenderLinesAfter(Block block) foreach (var line in block.LinesAfter) { Write(line); - WriteLine(line.Newline); + WriteLine(line.NewLine); } } } diff --git a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs index 2460b949b..3fb21017a 100644 --- a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs @@ -21,7 +21,7 @@ protected override void Write(RoundtripRenderer renderer, ThematicBreakBlock obj //renderer.Write(obj.BeforeWhitespace); renderer.Write(obj.Content); //renderer.Write(obj.AfterWhitespace); - renderer.WriteLine(obj.Newline); + renderer.WriteLine(obj.NewLine); renderer.RenderLinesAfter(obj); } } diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index c131ac835..810f4fb1e 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -296,10 +296,10 @@ public T WriteLine() /// /// This instance [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T WriteLine(Newline newline) + public T WriteLine(NewLine newLine) { WriteIndent(); - Writer.NewLine = newline.AsString(); + Writer.NewLine = newLine.AsString(); Writer.WriteLine(); previousWasLine = true; return (T)this; diff --git a/src/Markdig/Syntax/Block.cs b/src/Markdig/Syntax/Block.cs index dc1fd56c2..8c981591a 100644 --- a/src/Markdig/Syntax/Block.cs +++ b/src/Markdig/Syntax/Block.cs @@ -48,7 +48,7 @@ protected Block(BlockParser parser) /// /// The last newline of this block /// - public Newline Newline { get; set; } + public NewLine NewLine { get; set; } /// /// Gets or sets a value indicating whether this block must be removed from its container after inlines have been processed. diff --git a/src/Markdig/Syntax/CharIteratorHelper.cs b/src/Markdig/Syntax/CharIteratorHelper.cs index bc046e9d7..5edf20bed 100644 --- a/src/Markdig/Syntax/CharIteratorHelper.cs +++ b/src/Markdig/Syntax/CharIteratorHelper.cs @@ -28,27 +28,27 @@ public static bool TrimStartAndCountNewLines(ref T iterator, out int countNew return hasWhitespaces; } - public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines, out Newline firstNewline) where T : ICharIterator + public static bool TrimStartAndCountNewLines(ref T iterator, out int countNewLines, out NewLine firstNewline) where T : ICharIterator { countNewLines = 0; var c = iterator.CurrentChar; bool hasWhitespaces = false; - firstNewline = Newline.None; + firstNewline = NewLine.None; while (c.IsWhitespace()) { if (c == '\n' || c == '\r') { - if (c == '\r' && iterator.PeekChar() == '\n' && firstNewline != Newline.None) + if (c == '\r' && iterator.PeekChar() == '\n' && firstNewline != NewLine.None) { - firstNewline = Newline.CarriageReturnLineFeed; + firstNewline = NewLine.CarriageReturnLineFeed; } - else if (c == '\n' && firstNewline != Newline.None) + else if (c == '\n' && firstNewline != NewLine.None) { - firstNewline = Newline.LineFeed; + firstNewline = NewLine.LineFeed; } - else if (c == '\r' && firstNewline != Newline.None) + else if (c == '\r' && firstNewline != NewLine.None) { - firstNewline = Newline.CarriageReturn; + firstNewline = NewLine.CarriageReturn; } countNewLines++; } diff --git a/src/Markdig/Syntax/FencedCodeBlock.cs b/src/Markdig/Syntax/FencedCodeBlock.cs index 3262b4087..d7d7a037c 100644 --- a/src/Markdig/Syntax/FencedCodeBlock.cs +++ b/src/Markdig/Syntax/FencedCodeBlock.cs @@ -62,7 +62,7 @@ public FencedCodeBlock(BlockParser parser) : base(parser) public StringSlice TriviaAfterArguments { get; set; } /// - public Newline InfoNewline { get; set; } + public NewLine InfoNewLine { get; set; } /// public StringSlice TriviaBeforeClosingFence { get; set; } diff --git a/src/Markdig/Syntax/HeadingBlock.cs b/src/Markdig/Syntax/HeadingBlock.cs index f4e6417c5..5f8ba0186 100644 --- a/src/Markdig/Syntax/HeadingBlock.cs +++ b/src/Markdig/Syntax/HeadingBlock.cs @@ -46,7 +46,7 @@ public HeadingBlock(BlockParser parser) : base(parser) /// /// Gets or sets the newline of the first line when is true. /// - public Newline SetextNewline { get; set; } + public NewLine SetextNewline { get; set; } /// /// Gets or sets the whitespace after the # character when is false. diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index f7b3b9773..b6bab80d2 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -74,7 +74,7 @@ public interface IFencedBlock : IBlock /// Trivia: only parsed when is enabled, otherwise /// . /// - Newline InfoNewline { get; set; } + NewLine InfoNewLine { get; set; } /// /// Trivia before the closing fenced chars @@ -90,10 +90,10 @@ public interface IFencedBlock : IBlock /// /// Newline after the last line, which is always the line containing the closing fence chars. - /// "Inherited" from . + /// "Inherited" from . /// Trivia: only parsed when is enabled, otherwise /// . /// - Newline Newline { get; set; } + NewLine NewLine { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/LineBreakInline.cs b/src/Markdig/Syntax/Inlines/LineBreakInline.cs index 8093f950c..139c09a95 100644 --- a/src/Markdig/Syntax/Inlines/LineBreakInline.cs +++ b/src/Markdig/Syntax/Inlines/LineBreakInline.cs @@ -16,6 +16,6 @@ public class LineBreakInline : LeafInline public bool IsBackslash { get; set; } - public Newline Newline { get; set; } + public NewLine NewLine { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LeafBlock.cs b/src/Markdig/Syntax/LeafBlock.cs index ca8aaa8ff..fed2ee405 100644 --- a/src/Markdig/Syntax/LeafBlock.cs +++ b/src/Markdig/Syntax/LeafBlock.cs @@ -79,7 +79,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi { Lines = new StringLineGroup(4, ProcessInlines); } - var stringLine = new StringLine(ref slice, line, column, sourceLinePosition, slice.Newline); + var stringLine = new StringLine(ref slice, line, column, sourceLinePosition, slice.NewLine); // Regular case, we are not in the middle of a tab if (slice.CurrentChar != '\t' || !CharHelper.IsAcrossTab(column)) { @@ -101,7 +101,7 @@ public void AppendLine(ref StringSlice slice, int column, int line, int sourceLi stringLine.Slice = new StringSlice(builder.GetStringAndReset()); Lines.Add(ref stringLine); } - Newline = slice.Newline; // update newline, as it should be the last newline of the block + NewLine = slice.NewLine; // update newline, as it should be the last newline of the block } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/LinkReferenceDefinition.cs b/src/Markdig/Syntax/LinkReferenceDefinition.cs index 8b7caf6a3..1f4ab3d89 100644 --- a/src/Markdig/Syntax/LinkReferenceDefinition.cs +++ b/src/Markdig/Syntax/LinkReferenceDefinition.cs @@ -194,7 +194,7 @@ public static bool TryParseTrivia( out string title, out unescapedTitle, out char titleEnclosingCharacter, - out Newline newline, + out NewLine newLine, out triviaAfterTitle, out SourceSpan labelSpan, out SourceSpan urlSpan, @@ -214,7 +214,7 @@ public static bool TryParseTrivia( //UnescapedTitle = unescapedTitle, TitleSpan = titleSpan, Span = new SourceSpan(startSpan, titleSpan.End > 0 ? titleSpan.End : urlSpan.End), - Newline = newline, + NewLine = newLine, }; return true; } diff --git a/src/Markdig/Syntax/QuoteBlock.cs b/src/Markdig/Syntax/QuoteBlock.cs index e8c561c19..f4fa293ff 100644 --- a/src/Markdig/Syntax/QuoteBlock.cs +++ b/src/Markdig/Syntax/QuoteBlock.cs @@ -78,6 +78,6 @@ public class QuoteBlockLine /// Trivia: only parsed when is enabled, otherwise /// is empty. /// - public Newline Newline { get; set; } + public NewLine NewLine { get; set; } } } \ No newline at end of file From 763ed32212a1b2fe90d7ba6ebec5836717d9d857 Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Thu, 4 Mar 2021 22:42:48 +0100 Subject: [PATCH 117/120] NewLine renaming remnants --- src/Markdig/Helpers/LineReader.cs | 6 +++--- src/Markdig/Helpers/Newline.cs | 2 +- src/Markdig/Helpers/StringSlice.cs | 8 ++++---- src/Markdig/Parsers/Inlines/EscapeInlineParser.cs | 6 +++--- src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index a1196a526..27c32195c 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -48,15 +48,15 @@ public StringSlice ReadLine() if (c == '\r') { int length = 1; - var newline = NewLine.CarriageReturn; + var newLine = NewLine.CarriageReturn; if (c == '\r' && (uint)(i + 1) < (uint)text.Length && text[i + 1] == '\n') { i++; length = 2; - newline = NewLine.CarriageReturnLineFeed; + newLine = NewLine.CarriageReturnLineFeed; } - var slice = new StringSlice(text, sourcePosition, i - length, newline); + var slice = new StringSlice(text, sourcePosition, i - length, newLine); SourcePosition = i + 1; return slice; } diff --git a/src/Markdig/Helpers/Newline.cs b/src/Markdig/Helpers/Newline.cs index a87bb2c7b..989c0cbdb 100644 --- a/src/Markdig/Helpers/Newline.cs +++ b/src/Markdig/Helpers/Newline.cs @@ -33,7 +33,7 @@ public static string AsString(this NewLine newLine) return string.Empty; } - public static int Length(this NewLine newline) => newline switch + public static int Length(this NewLine newLine) => newLine switch { NewLine.None => 0, NewLine.CarriageReturn => 1, diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index 536046397..d13ab5b59 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -34,12 +34,12 @@ public StringSlice(string text) /// Initializes a new instance of the struct. /// /// The text. - public StringSlice(string text, NewLine newline) + public StringSlice(string text, NewLine newLine) { Text = text; Start = 0; End = (Text?.Length ?? 0) - 1; - NewLine = newline; + NewLine = newLine; } /// @@ -67,7 +67,7 @@ public StringSlice(string text, int start, int end) /// The start. /// The end. /// - public StringSlice(string text, int start, int end, NewLine newline) + public StringSlice(string text, int start, int end, NewLine newLine) { if (text is null) ThrowHelper.ArgumentNullException_text(); @@ -75,7 +75,7 @@ public StringSlice(string text, int start, int end, NewLine newline) Text = text; Start = start; End = end; - NewLine = newline; + NewLine = newLine; } /// diff --git a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs index aa9aae303..1cd4b7e65 100644 --- a/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EscapeInlineParser.cs @@ -45,10 +45,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { if (c == '\n' || c == '\r') { - var newline = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn; + var newLine = c == '\n' ? NewLine.LineFeed : NewLine.CarriageReturn; if (c == '\r' && slice.PeekChar() == '\n') { - newline = NewLine.CarriageReturnLineFeed; + newLine = NewLine.CarriageReturnLineFeed; } processor.Inline = new LineBreakInline() { @@ -57,7 +57,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) }, Line = line, Column = column, - NewLine = newline + NewLine = newLine }; processor.Inline.Span.End = processor.Inline.Span.Start + 1; slice.NextChar(); diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index 926a31274..0bcb60e0d 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -41,12 +41,12 @@ protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) { NewLine = quoteLine.NewLine }; - var newline = new LineBreakInline + var newLine = new LineBreakInline { NewLine = quoteLine.NewLine }; var container = new ContainerInline(); - container.AppendChild(newline); + container.AppendChild(newLine); emptyLeafBlock.Inline = container; quoteBlock.Add(emptyLeafBlock); } From 59df6d1a2e1af56fd1c44e77d542a3102cb1997a Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Thu, 4 Mar 2021 22:57:57 +0100 Subject: [PATCH 118/120] primarily define TrackTrivia on MarkdownPipeline --- .../Inlines/TestNullCharacterInline.cs | 3 ++- .../RoundtripSpecs/TestIndentedCodeBlock.cs | 3 ++- src/Markdig.Tests/TestRoundtrip.cs | 3 ++- src/Markdig/Markdown.cs | 11 ++++++++--- src/Markdig/MarkdownExtensions.cs | 15 +++++++++++++++ src/Markdig/MarkdownPipeline.cs | 6 ++++++ src/Markdig/MarkdownPipelineBuilder.cs | 9 ++++++++- src/Markdig/Parsers/MarkdownParser.cs | 16 ++++++++++------ 8 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs index ad21b9fa7..b40919e52 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -23,8 +23,9 @@ public void Test(string value, string expected) private static void RoundTrip(string markdown, string expected) { var pipelineBuilder = new MarkdownPipelineBuilder(); + pipelineBuilder.EnableTrackTrivia(); MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline, trackTrivia: true); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); var sw = new StringWriter(); var rr = new RoundtripRenderer(sw); diff --git a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs index c53ee5376..0a4935797 100644 --- a/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs +++ b/src/Markdig.Tests/RoundtripSpecs/TestIndentedCodeBlock.cs @@ -68,8 +68,9 @@ public void TestNewline(string value) public void TestNewlinesInBetweenResultInOneCodeBlock(string value) { var pipelineBuilder = new MarkdownPipelineBuilder(); + pipelineBuilder.EnableTrackTrivia(); MarkdownPipeline pipeline = pipelineBuilder.Build(); - var markdownDocument = Markdown.Parse(value, pipeline, trackTrivia: true); + var markdownDocument = Markdown.Parse(value, pipeline); Assert.AreEqual(1, markdownDocument.Count); } diff --git a/src/Markdig.Tests/TestRoundtrip.cs b/src/Markdig.Tests/TestRoundtrip.cs index 2f491530d..2a8c9a4b1 100644 --- a/src/Markdig.Tests/TestRoundtrip.cs +++ b/src/Markdig.Tests/TestRoundtrip.cs @@ -15,8 +15,9 @@ internal static void TestSpec(string markdownText, string expected, string exten internal static void RoundTrip(string markdown) { var pipelineBuilder = new MarkdownPipelineBuilder(); + pipelineBuilder.EnableTrackTrivia(); MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline, trackTrivia: true); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); var sw = new StringWriter(); var nr = new RoundtripRenderer(sw); diff --git a/src/Markdig/Markdown.cs b/src/Markdig/Markdown.cs index bb8b562bf..5e64626ba 100644 --- a/src/Markdig/Markdown.cs +++ b/src/Markdig/Markdown.cs @@ -142,7 +142,12 @@ public static object Convert(string markdown, IMarkdownRenderer renderer, Markdo public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) { if (markdown == null) ThrowHelper.ArgumentNullException_markdown(); - return Parse(markdown, null, trackTrivia: trackTrivia); + + MarkdownPipeline pipeline = trackTrivia ? new MarkdownPipelineBuilder() + .EnableTrackTrivia() + .Build() : null; + + return Parse(markdown, pipeline); } /// @@ -153,13 +158,13 @@ public static MarkdownDocument Parse(string markdown, bool trackTrivia = false) /// A parser context used for the parsing. /// An AST Markdown document /// if markdown variable is null - public static MarkdownDocument Parse(string markdown, MarkdownPipeline pipeline, MarkdownParserContext context = null, bool trackTrivia = false) + public static MarkdownDocument Parse(string markdown, MarkdownPipeline pipeline, MarkdownParserContext context = null) { if (markdown == null) ThrowHelper.ArgumentNullException_markdown(); pipeline ??= new MarkdownPipelineBuilder().Build(); pipeline = CheckForSelfPipeline(pipeline, markdown); - return MarkdownParser.Parse(markdown, pipeline, context, trackTrivia); + return MarkdownParser.Parse(markdown, pipeline, context); } private static MarkdownPipeline CheckForSelfPipeline(MarkdownPipeline pipeline, string markdown) diff --git a/src/Markdig/MarkdownExtensions.cs b/src/Markdig/MarkdownExtensions.cs index 8dd75e441..6a4e52868 100644 --- a/src/Markdig/MarkdownExtensions.cs +++ b/src/Markdig/MarkdownExtensions.cs @@ -677,5 +677,20 @@ public static MarkdownPipelineBuilder DisableHeadings(this MarkdownPipelineBuild } return pipeline; } + + /// + /// Enables parsing and tracking of trivia characters + /// + /// The pipeline. + /// he modified pipeline + public static MarkdownPipelineBuilder EnableTrackTrivia(this MarkdownPipelineBuilder pipeline) + { + pipeline.TrackTrivia = true; + if (pipeline.BlockParsers.TryFind(out var parser)) + { + parser.InfoParser = FencedCodeBlockParser.RoundtripInfoParser; + } + return pipeline; + } } } diff --git a/src/Markdig/MarkdownPipeline.cs b/src/Markdig/MarkdownPipeline.cs index 383ff00bc..07dfd2ba5 100644 --- a/src/Markdig/MarkdownPipeline.cs +++ b/src/Markdig/MarkdownPipeline.cs @@ -48,6 +48,12 @@ internal MarkdownPipeline(OrderedList extensions, BlockParse internal ProcessDocumentDelegate DocumentProcessed; + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; set; } + /// /// Allows to setup a . /// diff --git a/src/Markdig/MarkdownPipelineBuilder.cs b/src/Markdig/MarkdownPipelineBuilder.cs index 8bc659f15..05ca691da 100644 --- a/src/Markdig/MarkdownPipelineBuilder.cs +++ b/src/Markdig/MarkdownPipelineBuilder.cs @@ -76,6 +76,12 @@ public MarkdownPipelineBuilder() /// public TextWriter DebugLog { get; set; } + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// + public bool TrackTrivia { get; set; } + /// /// Occurs when a document has been processed after the method. /// @@ -116,7 +122,8 @@ public MarkdownPipeline Build() DebugLog, GetDocumentProcessed) { - PreciseSourceLocation = PreciseSourceLocation + PreciseSourceLocation = PreciseSourceLocation, + TrackTrivia = TrackTrivia, }; return pipeline; } diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index 7ee64d696..cab6ea984 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -26,6 +26,10 @@ public sealed class MarkdownParser private readonly ProcessDocumentDelegate documentProcessed; private readonly bool preciseSourceLocation; + /// + /// True to parse trivia such as whitespace, extra heading characters and unescaped + /// string values. + /// public bool TrackTrivia { get; } private readonly int roughLineCountEstimate; @@ -40,12 +44,12 @@ public sealed class MarkdownParser /// A parser context used for the parsing. /// /// - private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserContext context, bool trackTrivia = false) + private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserContext context) { if (text == null) ThrowHelper.ArgumentNullException_text(); if (pipeline == null) ThrowHelper.ArgumentNullException(nameof(pipeline)); - TrackTrivia = trackTrivia; + TrackTrivia = pipeline.TrackTrivia; roughLineCountEstimate = text.Length / 40; text = FixupZero(text); lineReader = new LineReader(text); @@ -55,10 +59,10 @@ private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserCon document = new MarkdownDocument(); // Initialize the block parsers - blockProcessor = new BlockProcessor(document, pipeline.BlockParsers, context, trackTrivia); + blockProcessor = new BlockProcessor(document, pipeline.BlockParsers, context, pipeline.TrackTrivia); // Initialize the inline parsers - inlineProcessor = new InlineProcessor(document, pipeline.InlineParsers, pipeline.PreciseSourceLocation, context, trackTrivia) + inlineProcessor = new InlineProcessor(document, pipeline.InlineParsers, pipeline.PreciseSourceLocation, context, pipeline.TrackTrivia) { DebugLog = pipeline.DebugLog }; @@ -74,13 +78,13 @@ private MarkdownParser(string text, MarkdownPipeline pipeline, MarkdownParserCon /// A parser context used for the parsing. /// An AST Markdown document /// if reader variable is null - public static MarkdownDocument Parse(string text, MarkdownPipeline pipeline = null, MarkdownParserContext context = null, bool trackTrivia = false) + public static MarkdownDocument Parse(string text, MarkdownPipeline pipeline = null, MarkdownParserContext context = null) { if (text == null) ThrowHelper.ArgumentNullException_text(); pipeline ??= new MarkdownPipelineBuilder().Build(); // Perform the parsing - var markdownParser = new MarkdownParser(text, pipeline, context, trackTrivia); + var markdownParser = new MarkdownParser(text, pipeline, context); return markdownParser.Parse(); } From 19e409ceca821c72615da86e1d1cd7d8e0cbf39c Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Thu, 4 Mar 2021 23:28:37 +0100 Subject: [PATCH 119/120] correctly calculate newlines in StringLineGroup PeekChar --- src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs | 6 ++++++ src/Markdig/Helpers/StringLineGroup.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs index a3a39ec20..16d5a2788 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLinkInline.cs @@ -219,5 +219,11 @@ public void Test_UncommonWhitespace(string value) { RoundTrip(value); } + + [TestCase("[x]: https://example.com\r\n")] + public void Test_LinkReferenceDefinitionWithCarriageReturnLineFeed(string value) + { + RoundTrip(value); + } } } diff --git a/src/Markdig/Helpers/StringLineGroup.cs b/src/Markdig/Helpers/StringLineGroup.cs index 6f53f75e9..940f9ed5c 100644 --- a/src/Markdig/Helpers/StringLineGroup.cs +++ b/src/Markdig/Helpers/StringLineGroup.cs @@ -331,7 +331,7 @@ public readonly char PeekChar(int offset = 1) int sliceIndex = SliceIndex; var line = _lines.Lines[sliceIndex]; var slice = line.Slice; - if (!(line.NewLine == NewLine.CarriageReturnLineFeed && offset + 1 == slice.Length)) + if (!(line.NewLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1)) { while (offset > slice.Length) { From bb42ee42ca34b50f7899ae961beea66177bdb9cb Mon Sep 17 00:00:00 2001 From: Ruud Poutsma Date: Sun, 7 Mar 2021 16:14:48 +0100 Subject: [PATCH 120/120] code review feedback --- .../Inlines/TestLiteralInline.cs | 10 ------- src/Markdig.Tests/TestClassAttribute.cs | 8 ----- src/Markdig.Tests/TestMediaLinks.cs | 30 +++++++++---------- src/Markdig/Helpers/CharHelper.cs | 2 +- src/Markdig/Helpers/LinkHelper.cs | 4 +-- src/Markdig/Helpers/Newline.cs | 2 +- src/Markdig/Parsers/QuoteBlockParser.cs | 1 - .../Renderers/Roundtrip/CodeBlockRenderer.cs | 4 +-- .../Renderers/Roundtrip/HeadingRenderer.cs | 2 +- .../Inlines/LineBreakInlineRenderer.cs | 6 ---- .../Renderers/Roundtrip/ListRenderer.cs | 2 +- .../Renderers/Roundtrip/ParagraphRenderer.cs | 2 +- .../Renderers/Roundtrip/QuoteBlockRenderer.cs | 3 +- .../Renderers/Roundtrip/RoundtripRenderer.cs | 8 +++-- .../Roundtrip/ThematicBreakRenderer.cs | 6 +--- src/Markdig/Roundtrip.md | 4 +-- src/Markdig/Syntax/IBlock.cs | 12 ++++++-- src/Markdig/Syntax/IFencedBlock.cs | 2 +- 18 files changed, 43 insertions(+), 65 deletions(-) delete mode 100644 src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs delete mode 100644 src/Markdig.Tests/TestClassAttribute.cs diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs deleted file mode 100644 index 50cf89795..000000000 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestLiteralInline.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Markdig.Tests.RoundtripSpecs.Inlines -{ - class TestLiteralInline - { - } -} diff --git a/src/Markdig.Tests/TestClassAttribute.cs b/src/Markdig.Tests/TestClassAttribute.cs deleted file mode 100644 index f0d30790e..000000000 --- a/src/Markdig.Tests/TestClassAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Markdig.Tests -{ - internal class TestClassAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/Markdig.Tests/TestMediaLinks.cs b/src/Markdig.Tests/TestMediaLinks.cs index bad7034bc..27ba34262 100644 --- a/src/Markdig.Tests/TestMediaLinks.cs +++ b/src/Markdig.Tests/TestMediaLinks.cs @@ -62,21 +62,21 @@ public TestHostProvider(string provider, string replace) } } - //[Test] - //[TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")] - //[TestCase("![p1](//sample.com/video.mp4)", "

\n", @"^//sample.com/(.+)$", @"https://example.com/$1")] - //[TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")] - //public void TestCustomHostProvider(string markdown, string expected, string provider, string replace) - //{ - // string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions - // { - // Hosts = - // { - // new TestHostProvider(provider, replace), - // } - // })); - // Assert.AreEqual(html, expected); - //} + [Test] + [TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")] + [TestCase("![p1](//sample.com/video.mp4)", "

\n", @"^//sample.com/(.+)$", @"https://example.com/$1")] + [TestCase("![p1](https://sample.com/video.mp4)", "

\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")] + public void TestCustomHostProvider(string markdown, string expected, string provider, string replace) + { + string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions + { + Hosts = + { + new TestHostProvider(provider, replace), + } + })); + Assert.AreEqual(html, expected); + } [Test] [TestCase("![static mp4](//sample.com/video.mp4)", "

\n", "")] diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index d4af040bf..c23293930 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -221,7 +221,7 @@ internal static bool IsSpaceOrPunctuation(this char c) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsNewLineLineFeed(this char c) + public static bool IsNewLineOrLineFeed(this char c) { return c == '\n' || c == '\r'; } diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 3679d7059..50f841d09 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -759,7 +759,7 @@ public static bool TryParseUrl(ref T text, out string link, out bool hasPoint continue; } - if (c.IsNewLineLineFeed()) + if (c.IsNewLineOrLineFeed()) { break; } @@ -902,7 +902,7 @@ public static bool TryParseUrlTrivia(ref T text, out string link, out bool ha continue; } - if (c.IsNewLineLineFeed()) + if (c.IsNewLineOrLineFeed()) { break; } diff --git a/src/Markdig/Helpers/Newline.cs b/src/Markdig/Helpers/Newline.cs index 989c0cbdb..ed232cb47 100644 --- a/src/Markdig/Helpers/Newline.cs +++ b/src/Markdig/Helpers/Newline.cs @@ -6,7 +6,7 @@ namespace Markdig.Helpers /// Represents a character or set of characters that represent a separation /// between two lines of text ///
- public enum NewLine + public enum NewLine : byte { None, CarriageReturn, diff --git a/src/Markdig/Parsers/QuoteBlockParser.cs b/src/Markdig/Parsers/QuoteBlockParser.cs index c1e8410f8..641c566a8 100644 --- a/src/Markdig/Parsers/QuoteBlockParser.cs +++ b/src/Markdig/Parsers/QuoteBlockParser.cs @@ -115,7 +115,6 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) c = processor.NextChar(); // Skip quote marker char if (c == ' ') { - //processor.NextChar(); processor.NextColumn(); hasSpaceAfterQuoteChar = true; processor.SkipFirstUnwindSpace = true; diff --git a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs index bbea06df2..a38c65e0e 100644 --- a/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/CodeBlockRenderer.cs @@ -8,13 +8,11 @@ namespace Markdig.Renderers.Roundtrip { /// - /// An Normalize renderer for a and . + /// An Roundtrip renderer for a and . /// /// public class CodeBlockRenderer : RoundtripObjectRenderer { - public bool OutputAttributesOnPre { get; set; } - protected override void Write(RoundtripRenderer renderer, CodeBlock obj) { renderer.RenderLinesBefore(obj); diff --git a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs index 2cd9f468d..b73fb7084 100644 --- a/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/HeadingRenderer.cs @@ -7,7 +7,7 @@ namespace Markdig.Renderers.Roundtrip { /// - /// An Normalize renderer for a . + /// An Roundtrip renderer for a . /// /// public class HeadingRenderer : RoundtripObjectRenderer diff --git a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs index 7edcd6574..2c47866d5 100644 --- a/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/Inlines/LineBreakInlineRenderer.cs @@ -12,17 +12,11 @@ namespace Markdig.Renderers.Roundtrip.Inlines /// public class LineBreakInlineRenderer : RoundtripObjectRenderer { - /// - /// Gets or sets a value indicating whether to render this softline break as a Normalize hardline break tag (<br />) - /// - public bool RenderAsHardlineBreak { get; set; } - protected override void Write(RoundtripRenderer renderer, LineBreakInline obj) { if (obj.IsHard && obj.IsBackslash) { renderer.Write("\\"); - //renderer.Write(obj.IsBackslash ? "\\" : " "); } renderer.WriteLine(obj.NewLine); } diff --git a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs index e08c252fc..340470e02 100644 --- a/src/Markdig/Renderers/Roundtrip/ListRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ListRenderer.cs @@ -9,7 +9,7 @@ namespace Markdig.Renderers.Roundtrip { /// - /// A Normalize renderer for a . + /// A Roundtrip renderer for a . /// /// public class ListRenderer : RoundtripObjectRenderer diff --git a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs index c0b8ed44b..4f439de45 100644 --- a/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ParagraphRenderer.cs @@ -8,7 +8,7 @@ namespace Markdig.Renderers.Roundtrip { /// - /// A Normalize renderer for a . + /// A Roundtrip renderer for a . /// /// [DebuggerDisplay("renderer.Writer.ToString()")] diff --git a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs index 0bcb60e0d..dbc92b349 100644 --- a/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/QuoteBlockRenderer.cs @@ -9,9 +9,8 @@ namespace Markdig.Renderers.Roundtrip { /// - /// A Normalize renderer for a . + /// A Roundtrip renderer for a . /// - /// public class QuoteBlockRenderer : RoundtripObjectRenderer { protected override void Write(RoundtripRenderer renderer, QuoteBlock quoteBlock) diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs index 2e0c7d638..07847e299 100644 --- a/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRenderer.cs @@ -6,19 +6,21 @@ using Markdig.Syntax; using Markdig.Renderers.Roundtrip.Inlines; using Markdig.Helpers; +using Markdig.Parsers; namespace Markdig.Renderers.Roundtrip { /// - /// Default HTML renderer for a Markdown object. + /// Markdown renderer honoring trivia for a object. /// + /// Ensure to enable the property when + /// parsing markdown to have trivia available for rendering. public class RoundtripRenderer : TextRendererBase { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The writer. - /// The normalize options public RoundtripRenderer(TextWriter writer) : base(writer) { // Default block renderers diff --git a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs index 3fb21017a..fda7caec8 100644 --- a/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Roundtrip/ThematicBreakRenderer.cs @@ -7,7 +7,7 @@ namespace Markdig.Renderers.Roundtrip { /// - /// A Normalize renderer for a . + /// A Roundtrip renderer for a . /// /// public class ThematicBreakRenderer : RoundtripObjectRenderer @@ -16,11 +16,7 @@ protected override void Write(RoundtripRenderer renderer, ThematicBreakBlock obj { renderer.RenderLinesBefore(obj); - // for now, render always a newline - // TODO: only render a newline when not last line - //renderer.Write(obj.BeforeWhitespace); renderer.Write(obj.Content); - //renderer.Write(obj.AfterWhitespace); renderer.WriteLine(obj.NewLine); renderer.RenderLinesAfter(obj); } diff --git a/src/Markdig/Roundtrip.md b/src/Markdig/Roundtrip.md index 4b9de0da3..9e39279ea 100644 --- a/src/Markdig/Roundtrip.md +++ b/src/Markdig/Roundtrip.md @@ -4,10 +4,10 @@ Markdig supports parsing trivia characters and tracks the source position of the To use this functionality, set the optional `trackTrivia` parameter to true when using the static `Markdown` class: ```csharp MarkdownDocument markdownDocument = Markdown.Parse(inputMarkdown, trackTrivia = true); -MarkdownDocument markdownDocument = Markdown.Parse(inputMarkdown, pipeline, trackTrivia = true); +true); ``` You will get a parse tree where various `Trivia*` properties of `Block` and `Inline` instances now have instances. To write a document to Markdown using this tree, use the `RoundtripRenderer`: -```csharp +```csharpTestLiteralInline var sw = new StringWriter(); var rr = new RoundtripRenderer(sw); rr.Write(markdownDocument); diff --git a/src/Markdig/Syntax/IBlock.cs b/src/Markdig/Syntax/IBlock.cs index ee79549c0..3196fb6ae 100644 --- a/src/Markdig/Syntax/IBlock.cs +++ b/src/Markdig/Syntax/IBlock.cs @@ -58,8 +58,16 @@ public interface IBlock : IMarkdownObject ///
event ProcessInlineDelegate ProcessInlinesEnd; - public StringSlice TriviaBefore { get; set; } - public StringSlice TriviaAfter { get; set; } + /// + /// Trivia occurring before this block + /// + /// Trivia: only parsed when is enabled, otherwise . + StringSlice TriviaBefore { get; set; } + /// + /// Trivia occurring after this block + /// + /// Trivia: only parsed when is enabled, otherwise . + StringSlice TriviaAfter { get; set; } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/IFencedBlock.cs b/src/Markdig/Syntax/IFencedBlock.cs index b6bab80d2..5a0230a75 100644 --- a/src/Markdig/Syntax/IFencedBlock.cs +++ b/src/Markdig/Syntax/IFencedBlock.cs @@ -20,7 +20,7 @@ public interface IFencedBlock : IBlock /// /// Gets or sets the fenced character count used to open this fenced code block. /// - public int OpeningFencedCharCount { get; set; } + int OpeningFencedCharCount { get; set; } /// /// Gets or sets the trivia after the .