diff --git a/src/Markdig.Tests/Markdig.Tests.csproj b/src/Markdig.Tests/Markdig.Tests.csproj index 0111c3e2b..10369fa28 100644 --- a/src/Markdig.Tests/Markdig.Tests.csproj +++ b/src/Markdig.Tests/Markdig.Tests.csproj @@ -76,6 +76,7 @@ + diff --git a/src/Markdig.Tests/TestEmphasisExtended.cs b/src/Markdig.Tests/TestEmphasisExtended.cs new file mode 100644 index 000000000..dff35c26c --- /dev/null +++ b/src/Markdig.Tests/TestEmphasisExtended.cs @@ -0,0 +1,168 @@ +using Markdig.Parsers.Inlines; +using Markdig.Renderers; +using Markdig.Renderers.Html; +using Markdig.Syntax.Inlines; +using NUnit.Framework; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestEmphasisExtended + { + class EmphasisTestExtension : IMarkdownExtension + { + public void Setup(MarkdownPipelineBuilder pipeline) + { + var emphasisParser = pipeline.InlineParsers.Find(); + Debug.Assert(emphasisParser != null); + + foreach (var emphasis in EmphasisTestDescriptors) + { + emphasisParser.EmphasisDescriptors.Add( + new EmphasisDescriptor(emphasis.Character, emphasis.Minimum, emphasis.Maximum, true)); + } + emphasisParser.TryCreateEmphasisInlineList.Add((delimiterChar, delimiterCount) => + { + return delimiterChar == '*' || delimiterChar == '_' + ? null + : new CustomEmphasisInline() { DelimiterChar = delimiterChar, DelimiterCount = delimiterCount }; + }); + } + + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + renderer.ObjectRenderers.Insert(0, new EmphasisRenderer()); + } + + class EmphasisRenderer : HtmlObjectRenderer + { + protected override void Write(HtmlRenderer renderer, CustomEmphasisInline obj) + { + var tag = EmphasisTestDescriptors.First(test => test.Character == obj.DelimiterChar).Tags[obj.DelimiterCount]; + + renderer.Write(tag.OpeningTag); + renderer.WriteChildren(obj); + renderer.Write(tag.ClosingTag); + } + } + } + class Tag + { +#pragma warning disable CS0649 + public int Level; +#pragma warning restore CS0649 + public string RawTag; + public string OpeningTag; + public string ClosingTag; + + public Tag(string tag) + { + RawTag = tag; + OpeningTag = "<" + tag + ">"; + ClosingTag = ""; + } + + public static implicit operator Tag(string tag) + => new Tag(tag); + } + class EmphasisTestDescriptor + { + public char Character; + public int Minimum; + public int Maximum; + public Dictionary Tags = new Dictionary(); + + private EmphasisTestDescriptor(char character, int min, int max) + { + Character = character; + Minimum = min; + Maximum = max; + } + public EmphasisTestDescriptor(char character, int min, int max, params Tag[] tags) + : this(character, min, max) + { + Debug.Assert(tags.Length == max - min + 1); + foreach (var tag in tags) + { + Tags.Add(min++, tag); + } + } + public EmphasisTestDescriptor(char character, int min, int max, string tag) + : this(character, min, max, new Tag(tag)) { } + } + class CustomEmphasisInline : EmphasisInline { } + static readonly EmphasisTestDescriptor[] EmphasisTestDescriptors = new[] + { + // Min Max + new EmphasisTestDescriptor('"', 1, 1, "quotation"), + new EmphasisTestDescriptor(',', 1, 2, "comma", "extra-comma"), + new EmphasisTestDescriptor('!', 2, 3, "warning", "error"), + new EmphasisTestDescriptor('=', 1, 3, "equal", "really-equal", "congruent"), + new EmphasisTestDescriptor('1', 1, 1, "one-only"), + new EmphasisTestDescriptor('2', 2, 2, "two-only"), + new EmphasisTestDescriptor('3', 3, 3, "three-only"), + }; + + static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Use().Build(); + + [Test] + [TestCase("*foo**", "foo*")] + [TestCase("**foo*", "*foo")] + [TestCase("***foo***", "foo")] + [TestCase("**_foo_**", "foo")] + [TestCase("_**foo**_", "foo")] + [TestCase("\"foo\"", "foo")] + [TestCase("\"\"foo\"\"", "foo")] + [TestCase("\"foo\"\"", "foo"")] + [TestCase("\"\"foo\"", ""foo")] + [TestCase(", foo", ", foo")] + [TestCase(", foo,", ", foo,")] + [TestCase(",some, foo,", "some foo,")] + [TestCase(",,foo,,", "foo")] + [TestCase(",foo,,", "foo,")] + [TestCase(",,,foo,,,", "foo")] + [TestCase("!1!", "!1!")] + [TestCase("!!2!!", "2")] + [TestCase("!!!3!!!", "3")] + [TestCase("!!!34!!!!", "34!")] + [TestCase("!!!!43!!!", "!43")] + [TestCase("!!!!44!!!!", "!44!")] // This is a new case - should the second ! be before or after ? + [TestCase("!!!!!5!!!!!", "5")] + [TestCase("!!!!!!6!!!!!!", "6")] + [TestCase("!! !mixed!!!", "!! !mixed!!!")] // can't open the delimiter because of the whitespace + [TestCase("=", "=")] + [TestCase("==", "==")] + [TestCase("====", "====")] + [TestCase("=a", "=a")] + [TestCase("=a=", "a")] + [TestCase("==a=", "=a")] + [TestCase("==a==", "a")] + [TestCase("==a===", "a=")] + [TestCase("===a===", "a")] + [TestCase("====a====", "a")] + [TestCase("=====a=====", "a")] + [TestCase("1", "1")] + [TestCase("1 1", "1 1")] + [TestCase("1Foo1", "Foo")] + [TestCase("1121", "12")] + [TestCase("22322", "3")] + [TestCase("2232", "2232")] + [TestCase("333", "333")] + [TestCase("3334333", "4")] + [TestCase("33334333", "34")] + [TestCase("33343333", "43")] + [TestCase("122122", "2222")] + [TestCase("221221", "11")] + [TestCase("122foo221", "foo")] + [TestCase("122foo122", "22foo22")] + [TestCase("!!!!!Attention:!! \"==1+1== 2\",but ===333 and 222===, mod 111!!!", + "Attention: + 2but 333 and 222 mod 111")] + public void TestEmphasis(string markdown, string expectedHtml) + { + TestParser.TestSpec(markdown, "

" + expectedHtml + "

", Pipeline); + } + } +} diff --git a/src/Markdig.Tests/TestEmphasisPlus.cs b/src/Markdig.Tests/TestEmphasisPlus.cs index 440230310..4822f543e 100644 --- a/src/Markdig.Tests/TestEmphasisPlus.cs +++ b/src/Markdig.Tests/TestEmphasisPlus.cs @@ -1,8 +1,6 @@ -ο»Ώ// 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. - -using System; using NUnit.Framework; namespace Markdig.Tests diff --git a/src/Markdig.Tests/TextAssert.cs b/src/Markdig.Tests/TextAssert.cs index 5f9c78955..1521736d8 100644 --- a/src/Markdig.Tests/TextAssert.cs +++ b/src/Markdig.Tests/TextAssert.cs @@ -22,12 +22,12 @@ public enum DiffStyle public static void AreEqual(string expectedValue, string actualValue) { - AreEqual(actualValue, expectedValue, DiffStyle.Full, Console.Out); + AreEqual(expectedValue, actualValue, DiffStyle.Full, Console.Out); } public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle) { - AreEqual(actualValue, expectedValue, diffStyle, Console.Out); + AreEqual(expectedValue, actualValue, diffStyle, Console.Out); } public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle, TextWriter output) diff --git a/src/Markdig/Extensions/Citations/CitationExtension.cs b/src/Markdig/Extensions/Citations/CitationExtension.cs index 296e3186b..7d04b1532 100644 --- a/src/Markdig/Extensions/Citations/CitationExtension.cs +++ b/src/Markdig/Extensions/Citations/CitationExtension.cs @@ -6,13 +6,14 @@ using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; using Markdig.Syntax.Inlines; - +using System.Diagnostics; + namespace Markdig.Extensions.Citations { /// /// Extension for cite ""..."" /// - /// + /// public class CitationExtension : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) @@ -26,8 +27,7 @@ public void Setup(MarkdownPipelineBuilder pipeline) public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { - var htmlRenderer = renderer as HtmlRenderer; - if (htmlRenderer != null) + if (renderer is HtmlRenderer htmlRenderer) { // Extend the rendering here. var emphasisRenderer = renderer.ObjectRenderers.FindExact(); @@ -42,7 +42,8 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) private static string GetTag(EmphasisInline emphasisInline) { - return emphasisInline.IsDouble && emphasisInline.DelimiterChar == '"' ? "cite" : null; + Debug.Assert(emphasisInline.DelimiterCount <= 2); + return emphasisInline.DelimiterCount == 2 && emphasisInline.DelimiterChar == '"' ? "cite" : null; } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs b/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs index 7876b9511..bd85868aa 100644 --- a/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs +++ b/src/Markdig/Extensions/CustomContainers/CustomContainerExtension.cs @@ -1,16 +1,16 @@ -ο»Ώ// 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. using Markdig.Parsers.Inlines; -using Markdig.Renderers; - +using Markdig.Renderers; + namespace Markdig.Extensions.CustomContainers { /// /// Extension to allow custom containers. /// - /// + /// public class CustomContainerExtension : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) @@ -26,22 +26,20 @@ public void Setup(MarkdownPipelineBuilder pipeline) if (inlineParser != null && !inlineParser.HasEmphasisChar(':')) { inlineParser.EmphasisDescriptors.Add(new EmphasisDescriptor(':', 2, 2, true)); - var previousCreateEmphasisInline = inlineParser.CreateEmphasisInline; - inlineParser.CreateEmphasisInline = (emphasisChar, strong) => - { - if (strong && emphasisChar == ':') - { - return new CustomContainerInline(); - } - return previousCreateEmphasisInline?.Invoke(emphasisChar, strong); - }; + inlineParser.TryCreateEmphasisInlineList.Add((emphasisChar, delimiterCount) => + { + if (delimiterCount == 2 && emphasisChar == ':') + { + return new CustomContainerInline(); + } + return null; + }); } } public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { - var htmlRenderer = renderer as HtmlRenderer; - if (htmlRenderer != null) + if (renderer is HtmlRenderer htmlRenderer) { if (!htmlRenderer.ObjectRenderers.Contains()) { diff --git a/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs b/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs index 63bbb1ca9..e659f20db 100644 --- a/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs +++ b/src/Markdig/Extensions/CustomContainers/HtmlCustomContainerInlineRenderer.cs @@ -9,7 +9,7 @@ namespace Markdig.Extensions.CustomContainers /// /// A HTML renderer for a . /// - /// + /// public class HtmlCustomContainerInlineRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) diff --git a/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs b/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs index 0b915e937..d3d06a6c3 100644 --- a/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs +++ b/src/Markdig/Extensions/EmphasisExtras/EmphasisExtraExtension.cs @@ -6,13 +6,14 @@ using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; using Markdig.Syntax.Inlines; - +using System.Diagnostics; + namespace Markdig.Extensions.EmphasisExtras { /// /// Extension for strikethrough, subscript, superscript, inserted and marked. /// - /// + /// public class EmphasisExtraExtension : IMarkdownExtension { /// @@ -89,8 +90,7 @@ public void Setup(MarkdownPipelineBuilder pipeline) public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { - var htmlRenderer = renderer as HtmlRenderer; - if (htmlRenderer != null) + if (renderer is HtmlRenderer htmlRenderer) { // Extend the rendering here. var emphasisRenderer = htmlRenderer.ObjectRenderers.FindExact(); @@ -107,8 +107,9 @@ private string GetTag(EmphasisInline emphasisInline) var c = emphasisInline.DelimiterChar; switch (c) { - case '~': - return emphasisInline.IsDouble ? "del" : "sub"; + case '~': + Debug.Assert(emphasisInline.DelimiterCount <= 2); + return emphasisInline.DelimiterCount == 2 ? "del" : "sub"; case '^': return "sup"; case '+': diff --git a/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs b/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs index 448d09b79..28b1f47a6 100644 --- a/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs +++ b/src/Markdig/Extensions/SmartyPants/SmartyPantsInlineParser.cs @@ -26,15 +26,15 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { // We are matching the following characters: // - // ' ‘ ’ ‘ ’ 'left-single-quote', 'right-single-quote' - // '' “ ” “ ” 'left-double-quote', 'right-double-quote' - // " “ ” “ ” 'left-double-quote', 'right-double-quote' - // << >> « » « » 'left-angle-quote', 'right-angle-quote' - // ... … … 'ellipsis' + // ' β€˜ ’ ‘ ’ 'left-single-quote', 'right-single-quote' + // '' β€œ ” “ ” 'left-double-quote', 'right-double-quote' + // " β€œ ” “ ” 'left-double-quote', 'right-double-quote' + // << >> Β« Β» « » 'left-angle-quote', 'right-angle-quote' + // ... … … 'ellipsis' // Special case: – and — are handle as a PostProcess step to avoid conflicts with pipetables header separator row - // -- – – 'ndash' - // --- — — 'mdash' + // -- – – 'ndash' + // --- β€” — 'mdash' var pc = slice.PeekCharExtra(-1); var c = slice.CurrentChar; diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index 5125abd96..5e6da0bdd 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -71,12 +71,8 @@ public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWor // (b) either not followed by a punctuation character, or 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. - bool nextIsPunctuation; - bool nextIsWhiteSpace; - bool prevIsPunctuation; - bool prevIsWhiteSpace; - pc.CheckUnicodeCategory(out prevIsWhiteSpace, out prevIsPunctuation); - c.CheckUnicodeCategory(out nextIsWhiteSpace, out nextIsPunctuation); + pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); + c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc); var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c); diff --git a/src/Markdig/Markdig.csproj b/src/Markdig/Markdig.csproj index 2ff3212a6..c9d980d18 100644 --- a/src/Markdig/Markdig.csproj +++ b/src/Markdig/Markdig.csproj @@ -18,6 +18,7 @@ https://github.com/lunet-io/markdig 1.6.0 true + 7.3 diff --git a/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs b/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs index 50228094f..5c76d64ba 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisDescriptor.cs @@ -23,7 +23,6 @@ public EmphasisDescriptor(char character, int minimumCount, int maximumCount, bo if (minimumCount < 1) throw new ArgumentOutOfRangeException(nameof(minimumCount), "minimumCount must be >= 1"); if (maximumCount < 1) throw new ArgumentOutOfRangeException(nameof(maximumCount), "maximumCount must be >= 1"); if (minimumCount > maximumCount) throw new ArgumentOutOfRangeException(nameof(minimumCount), "minimumCount must be <= maximumCount"); - if (maximumCount > 2) throw new ArgumentOutOfRangeException(nameof(maximumCount), "maximum must be <= 2"); Character = character; MinimumCount = minimumCount; @@ -42,7 +41,7 @@ public EmphasisDescriptor(char character, int minimumCount, int maximumCount, bo public readonly int MinimumCount; /// - /// The maximum number of character this emphasis is expected to have (must be >=1 and >= minumunCount and <= 2) + /// The maximum number of character this emphasis is expected to have (must be >=1 and >= minumunCount) /// public readonly int MaximumCount; diff --git a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs index 327ffeeab..888845aa4 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Markdig.Helpers; using Markdig.Renderers.Html; using Markdig.Syntax; @@ -14,14 +15,16 @@ namespace Markdig.Parsers.Inlines /// /// An inline parser for . /// - /// + /// /// public class EmphasisInlineParser : InlineParser, IPostInlineProcessor { private CharacterMap emphasisMap; - private readonly DelimitersObjectCache inlinesCache; + private readonly DelimitersObjectCache inlinesCache = new DelimitersObjectCache(); + [Obsolete("Use TryCreateEmphasisInlineDelegate instead", error: false)] public delegate EmphasisInline CreateEmphasisInlineDelegate(char emphasisChar, bool isStrong); + public delegate EmphasisInline TryCreateEmphasisInlineDelegate(char emphasisChar, int delimiterCount); /// /// Initializes a new instance of the class. @@ -33,7 +36,6 @@ public EmphasisInlineParser() new EmphasisDescriptor('*', 1, 2, true), new EmphasisDescriptor('_', 1, 2, false) }; - inlinesCache = new DelimitersObjectCache(); } /// @@ -61,7 +63,9 @@ public bool HasEmphasisChar(char c) /// /// Gets or sets the create emphasis inline delegate (allowing to create a different emphasis inline class) /// + [Obsolete("Use TryCreateEmphasisInlineList instead", error: false)] public CreateEmphasisInlineDelegate CreateEmphasisInline { get; set; } + public readonly List TryCreateEmphasisInlineList = new List(); public override void Initialize() { @@ -87,17 +91,16 @@ public override void Initialize() public bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex, bool isFinalProcessing) { - var container = root as ContainerInline; - if (container == null) + if (!(root is ContainerInline container)) { return true; } List delimiters = null; - if (container is EmphasisDelimiterInline) + if (container is EmphasisDelimiterInline emphasisDelimiter) { delimiters = inlinesCache.Get(); - delimiters.Add((EmphasisDelimiterInline)container); + delimiters.Add(emphasisDelimiter); } // Move current_position forward in the delimiter stack (if needed) until @@ -109,8 +112,7 @@ public bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, in { break; } - var delimiter = child as EmphasisDelimiterInline; - if (delimiter != null) + if (child is EmphasisDelimiterInline delimiter) { if (delimiters == null) { @@ -131,18 +133,17 @@ public bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, in } public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // First, some definitions. A delimiter run is either a sequence of one or more * characters that - // is not preceded or followed by a * character, or a sequence of one or more _ characters that - // is not preceded or followed by a _ character. + { + // First, some definitions. + // A delimiter run is a sequence of one or more delimiter characters that is not preceded or followed by the same delimiter character + // The amount of delimiter characters in the delimiter run may exceed emphasisDesc.MaximumCount, as that is handeled in `ProcessEmphasis` var delimiterChar = slice.CurrentChar; var emphasisDesc = emphasisMap[delimiterChar]; - var pc = (char)0; - if (processor.Inline is HtmlEntityInline) + char pc = (char)0; + if (processor.Inline is HtmlEntityInline htmlEntityInline) { - var htmlEntityInline = (HtmlEntityInline) processor.Inline; if (htmlEntityInline.Transcoded.Length > 0) { pc = htmlEntityInline.Transcoded[htmlEntityInline.Transcoded.End]; @@ -152,7 +153,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { pc = slice.PeekCharExtra(-1); if (pc == delimiterChar && slice.PeekCharExtra(-2) != '\\') - { + { + // If we get here, we determined that either: + // a) there weren't enough delimiters in the delimiter run to satisfy the MinimumCount condition + // b) the previous character couldn't open/close return false; } } @@ -174,38 +178,26 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } // The following character is actually an entity, we need to decode it - int htmlLength; - string htmlString; - if (HtmlEntityParser.TryParse(ref slice, out htmlString, out htmlLength)) + if (HtmlEntityParser.TryParse(ref slice, out string htmlString, out int htmlLength)) { c = htmlString[0]; } // Calculate Open-Close for current character - bool canOpen; - bool canClose; - CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out canOpen, out canClose); + CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out bool canOpen, out bool canClose); // We have potentially an open or close emphasis if (canOpen || canClose) { var delimiterType = DelimiterType.Undefined; - if (canOpen) - { - delimiterType |= DelimiterType.Open; - } - if (canClose) - { - delimiterType |= DelimiterType.Close; - } + if (canOpen) delimiterType |= DelimiterType.Open; + if (canClose) delimiterType |= DelimiterType.Close; - int line; - int column; var delimiter = new EmphasisDelimiterInline(this, emphasisDesc) { DelimiterCount = delimiterCount, Type = delimiterType, - Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.Start - 1)), + Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), Column = column, Line = line, }; @@ -223,25 +215,27 @@ private void ProcessEmphasis(InlineProcessor processor, List 0) + if ((closeDelimiter.Type & DelimiterType.Close) != 0 && closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount) { while (true) { // 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). EmphasisDelimiterInline openDelimiter = null; - EmphasisDescriptor emphasisDesc = null; int openDelimiterIndex = -1; for (int j = i - 1; j >= 0; j--) { @@ -254,32 +248,54 @@ private void ProcessEmphasis(InlineProcessor processor, List 0 && !isOddMatch) + previousOpenDelimiter.DelimiterCount >= emphasisDesc.MinimumCount && !isOddMatch) { openDelimiter = previousOpenDelimiter; - emphasisDesc = emphasisMap[previousOpenDelimiter.DelimiterChar]; openDelimiterIndex = j; break; } } if (openDelimiter != null) - { - process_delims: - bool isStrong = openDelimiter.DelimiterCount >= 2 && closeDelimiter.DelimiterCount >= 2 && emphasisDesc.MaximumCount >= 2; - - // Insert an emph or strong emph node accordingly, after the text node corresponding to the opener. - var emphasis = CreateEmphasisInline?.Invoke(closeDelimiter.DelimiterChar, isStrong) - ?? new EmphasisInline() - { - DelimiterChar = closeDelimiter.DelimiterChar, - IsDouble = isStrong - }; + { + process_delims: + Debug.Assert(openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now"); + Debug.Assert(closeDelimiter.DelimiterCount >= emphasisDesc.MinimumCount, "Extra emphasis should have been discarded by now"); + int delimiterDelta = Math.Min(Math.Min(openDelimiter.DelimiterCount, closeDelimiter.DelimiterCount), emphasisDesc.MaximumCount); + + // Insert an emph or strong emph node accordingly, after the text node corresponding to the opener. + EmphasisInline emphasis = null; + { + if (delimiterDelta <= 2) // We can try using the legacy delegate + { + #pragma warning disable CS0618 // Support fields marked as obsolete + emphasis = CreateEmphasisInline?.Invoke(closeDelimiter.DelimiterChar, isStrong: delimiterDelta == 2); + #pragma warning restore CS0618 // Support fields marked as obsolete + } + if (emphasis == null) + { + // Go in backwards order to give priority to newer delegates + for (int delegateIndex = TryCreateEmphasisInlineList.Count - 1; delegateIndex >= 0; delegateIndex--) + { + emphasis = TryCreateEmphasisInlineList[delegateIndex].Invoke(closeDelimiter.DelimiterChar, delimiterDelta); + if (emphasis != null) break; + } + + if (emphasis == null) + { + emphasis = new EmphasisInline() + { + DelimiterChar = closeDelimiter.DelimiterChar, + DelimiterCount = delimiterDelta + }; + } + } + } + Debug.Assert(emphasis != null); // Update position for emphasis var openDelimitercount = openDelimiter.DelimiterCount; var closeDelimitercount = closeDelimiter.DelimiterCount; - var delimiterDelta = isStrong ? 2 : 1; emphasis.Span.Start = openDelimiter.Span.Start; emphasis.Line = openDelimiter.Line; @@ -310,17 +326,7 @@ private void ProcessEmphasis(InlineProcessor processor, List= openDelimiterIndex + 1; k--) { var literalDelimiter = delimiters[k]; - var literal = new LiteralInline() - { - Content = new StringSlice(literalDelimiter.ToLiteral()), - IsClosed = true, - Span = literalDelimiter.Span, - Line = literalDelimiter.Line, - Column = literalDelimiter.Column - }; - - literalDelimiter.ReplaceBy(literal); - + literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline()); delimiters.RemoveAt(k); i--; } @@ -345,10 +351,17 @@ private void ProcessEmphasis(InlineProcessor processor, List 0) + if (openDelimiter.DelimiterCount >= emphasisDesc.MinimumCount) { goto process_delims; } + else if (openDelimiter.DelimiterCount > 0) + { + // There are still delimiter characters left, there's just not enough of them + openDelimiter.ReplaceBy(openDelimiter.AsLiteralInline()); + delimiters.RemoveAt(openDelimiterIndex); + i--; + } else { // Remove the open delimiter if it is also empty @@ -364,17 +377,7 @@ private void ProcessEmphasis(InlineProcessor processor, List /// A HTML renderer for an . /// - /// + /// public class EmphasisInlineRenderer : HtmlObjectRenderer { /// @@ -55,7 +56,8 @@ public string GetDefaultTag(EmphasisInline obj) { if (obj.DelimiterChar == '*' || obj.DelimiterChar == '_') { - return obj.IsDouble ? "strong" : "em"; + Debug.Assert(obj.DelimiterCount <= 2); + return obj.DelimiterCount == 2 ? "strong" : "em"; } return null; } diff --git a/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs index 36633f99d..a258ffd93 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/EmphasisInlineRenderer.cs @@ -8,12 +8,12 @@ namespace Markdig.Renderers.Normalize.Inlines /// /// A Normalize renderer for an . /// - /// + /// public class EmphasisInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, EmphasisInline obj) { - var emphasisText = new string(obj.DelimiterChar, obj.IsDouble ? 2 : 1); + var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount); renderer.Write(emphasisText); renderer.WriteChildren(obj); renderer.Write(emphasisText); diff --git a/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs b/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs index dd2291c53..b1e908c67 100644 --- a/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using System; +using Markdig.Helpers; using Markdig.Parsers; using Markdig.Parsers.Inlines; @@ -11,7 +12,7 @@ namespace Markdig.Syntax.Inlines /// /// A delimiter used for parsing emphasis. /// - /// + /// public class EmphasisDelimiterInline : DelimiterInline { /// @@ -19,11 +20,10 @@ public class EmphasisDelimiterInline : DelimiterInline /// /// The parser. /// The descriptor. - /// + /// public EmphasisDelimiterInline(InlineParser parser, EmphasisDescriptor descriptor) : base(parser) { - if (descriptor == null) throw new ArgumentNullException(nameof(descriptor)); - Descriptor = descriptor; + Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); DelimiterChar = descriptor.Character; } @@ -46,5 +46,17 @@ public override string ToLiteral() { return DelimiterCount > 0 ? new string(DelimiterChar, DelimiterCount) : string.Empty; } + + public LiteralInline AsLiteralInline() + { + return new LiteralInline() + { + Content = new StringSlice(ToLiteral()), + IsClosed = true, + Span = Span, + Line = Line, + Column = Column + }; + } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/EmphasisInline.cs b/src/Markdig/Syntax/Inlines/EmphasisInline.cs index 80d7f03d9..65dd5a1c4 100644 --- a/src/Markdig/Syntax/Inlines/EmphasisInline.cs +++ b/src/Markdig/Syntax/Inlines/EmphasisInline.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 System; using System.Diagnostics; namespace Markdig.Syntax.Inlines @@ -9,8 +10,8 @@ namespace Markdig.Syntax.Inlines /// /// An emphasis and strong emphasis (Section 6.4 CommonMark specs). /// - /// - [DebuggerDisplay("{DelimiterChar} Strong: {IsDouble}")] + /// + [DebuggerDisplay("{DelimiterChar} Count: {DelimiterCount}")] public class EmphasisInline : ContainerInline { /// @@ -20,7 +21,18 @@ public class EmphasisInline : ContainerInline /// /// Gets or sets a value indicating whether this is strong. + /// Marked obsolete as EmphasisInline can now be represented by more than two delimiter characters /// - public bool IsDouble { get; set; } + [Obsolete("Use `DelimiterCount == 2` instead", error: false)] + public bool IsDouble + { + get => DelimiterCount == 2; + set => DelimiterCount = value ? 2 : 1; + } + + /// + /// Gets or sets the number of delimiter characters for this emphasis. + /// + public int DelimiterCount { get; set; } } } \ No newline at end of file