diff --git a/src/Textamina.Markdig.Tests/Specs/SmartyPantsSpecs.md b/src/Textamina.Markdig.Tests/Specs/SmartyPantsSpecs.md index a83ad371..dc2d1f0d 100644 --- a/src/Textamina.Markdig.Tests/Specs/SmartyPantsSpecs.md +++ b/src/Textamina.Markdig.Tests/Specs/SmartyPantsSpecs.md @@ -2,7 +2,7 @@ Adds support for smarty pants: -## SmartyPants +## SmartyPants Quotes Converts the following character to smarty pants: @@ -24,6 +24,90 @@ This is a <>

This is a «text»

```````````````````````````````` +Unbalanced quotes are not changed: + +```````````````````````````````` example +This is a "text +. +

This is a "text

+```````````````````````````````` + +```````````````````````````````` example +This is a 'text +. +

This is a 'text

+```````````````````````````````` + +```````````````````````````````` example +This is a <This is a <<text

+```````````````````````````````` + +Unbalanced quotes inside other quotes are not changed: + +```````````````````````````````` example +This is a "text 'with" a another text' +. +

This is a “text 'with” a another text'

+```````````````````````````````` + +```````````````````````````````` example +This is a 'text <> +. +

This is a ‘text <<with’ a another text>>

+```````````````````````````````` + +```````````````````````````````` example +This is a <> a another text' +. +

This is a «text 'with» a another text'

+```````````````````````````````` + +Quotes requires to have the same rules than emphasis `_` regarding left/right frankling rules: + +```````````````````````````````` example +It's not quotes' +. +

It's not quotes'

+```````````````````````````````` + +```````````````````````````````` example +They are ' not matching quotes ' +. +

They are ' not matching quotes '

+```````````````````````````````` + +```````````````````````````````` example +They are' not matching 'quotes +. +

They are' not matching 'quotes

+```````````````````````````````` + +Double quotes using ``` `` ``` are working if they match another `''` pair, and there is no other double quotes on the line (otherwise they would be parsed as a code span): + +```````````````````````````````` example +This is ``a double quote'' +. +

This is “a double quote”

+```````````````````````````````` + +```````````````````````````````` example +This is ``a code span''`` +. +

This is a code span''

+```````````````````````````````` + +An emphasis starting inside left/right quotes will span over the right quote: + +```````````````````````````````` example +This is "a *text" with an emphasis* +. +

This is “a text” with an emphasis

+```````````````````````````````` + +## SmartyPants Separators + ```````````````````````````````` example This is a -- text . diff --git a/src/Textamina.Markdig.Tests/Specs/Specs.cs b/src/Textamina.Markdig.Tests/Specs/Specs.cs index 31e2175c..0df115cf 100644 --- a/src/Textamina.Markdig.Tests/Specs/Specs.cs +++ b/src/Textamina.Markdig.Tests/Specs/Specs.cs @@ -18439,17 +18439,17 @@ namespace Textamina.Markdig.Tests // // Adds support for smarty pants: // - // ## SmartyPants + // ## SmartyPants Quotes // // Converts the following character to smarty pants: [TestFixture] - public partial class TestExtensionsSmartyPants + public partial class TestExtensionsSmartyPantsQuotes { [Test] public void Example001() { // Example 1 - // Section: Extensions SmartyPants + // Section: Extensions SmartyPants Quotes // // The following CommonMark: // This is a "text" @@ -18457,18 +18457,18 @@ namespace Textamina.Markdig.Tests // Should be rendered as: //

This is a “text”

- Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 1, "Extensions SmartyPants"); + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 1, "Extensions SmartyPants Quotes"); TestParser.TestSpec("This is a \"text\"", "

This is a “text”

", "smartypants"); } } [TestFixture] - public partial class TestExtensionsSmartyPants + public partial class TestExtensionsSmartyPantsQuotes { [Test] public void Example002() { // Example 2 - // Section: Extensions SmartyPants + // Section: Extensions SmartyPants Quotes // // The following CommonMark: // This is a 'text' @@ -18476,37 +18476,271 @@ namespace Textamina.Markdig.Tests // Should be rendered as: //

This is a ‘text’

- Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 2, "Extensions SmartyPants"); + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 2, "Extensions SmartyPants Quotes"); TestParser.TestSpec("This is a 'text'", "

This is a ‘text’

", "smartypants"); } } [TestFixture] - public partial class TestExtensionsSmartyPants + public partial class TestExtensionsSmartyPantsQuotes { [Test] public void Example003() { // Example 3 - // Section: Extensions SmartyPants + // Section: Extensions SmartyPants Quotes // // The following CommonMark: // This is a <> // // Should be rendered as: - //

This is a “text”

+ //

This is a «text»

- Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 3, "Extensions SmartyPants"); - TestParser.TestSpec("This is a <>", "

This is a “text”

", "smartypants"); + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 3, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a <>", "

This is a «text»

", "smartypants"); } } + // Unbalanced quotes are not changed: [TestFixture] - public partial class TestExtensionsSmartyPants + public partial class TestExtensionsSmartyPantsQuotes { [Test] public void Example004() { // Example 4 - // Section: Extensions SmartyPants + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is a "text + // + // Should be rendered as: + //

This is a "text

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 4, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a \"text", "

This is a "text

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example005() + { + // Example 5 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is a 'text + // + // Should be rendered as: + //

This is a 'text

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 5, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a 'text", "

This is a 'text

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example006() + { + // Example 6 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is a <This is a <<text

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 6, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a <This is a <<text

", "smartypants"); + } + } + // Unbalanced quotes inside other quotes are not changed: + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example007() + { + // Example 7 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is a "text 'with" a another text' + // + // Should be rendered as: + //

This is a “text 'with” a another text'

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 7, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a \"text 'with\" a another text'", "

This is a “text 'with” a another text'

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example008() + { + // Example 8 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is a 'text <> + // + // Should be rendered as: + //

This is a ‘text <<with’ a another text>>

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 8, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a 'text <>", "

This is a ‘text <<with’ a another text>>

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example009() + { + // Example 9 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is a <> a another text' + // + // Should be rendered as: + //

This is a «text 'with» a another text'

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 9, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is a <> a another text'", "

This is a «text 'with» a another text'

", "smartypants"); + } + } + // Quotes requires to have the same rules than emphasis `_` regarding left/right frankling rules: + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example010() + { + // Example 10 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // It's not quotes' + // + // Should be rendered as: + //

It's not quotes'

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 10, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("It's not quotes'", "

It's not quotes'

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example011() + { + // Example 11 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // They are ' not matching quotes ' + // + // Should be rendered as: + //

They are ' not matching quotes '

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 11, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("They are ' not matching quotes '", "

They are ' not matching quotes '

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example012() + { + // Example 12 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // They are' not matching 'quotes + // + // Should be rendered as: + //

They are' not matching 'quotes

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 12, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("They are' not matching 'quotes", "

They are' not matching 'quotes

", "smartypants"); + } + } + // Double quotes using ``` `` ``` are working if they match another `''` pair, and there is no other double quotes on the line (otherwise they would be parsed as a code span): + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example013() + { + // Example 13 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is ``a double quote'' + // + // Should be rendered as: + //

This is “a double quote”

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 13, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is ``a double quote''", "

This is “a double quote”

", "smartypants"); + } + } + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example014() + { + // Example 14 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is ``a code span''`` + // + // Should be rendered as: + //

This is a code span''

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 14, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is ``a code span''`` ", "

This is a code span''

", "smartypants"); + } + } + // An emphasis starting inside left/right quotes will span over the right quote: + [TestFixture] + public partial class TestExtensionsSmartyPantsQuotes + { + [Test] + public void Example015() + { + // Example 15 + // Section: Extensions SmartyPants Quotes + // + // The following CommonMark: + // This is "a *text" with an emphasis* + // + // Should be rendered as: + //

This is “a text” with an emphasis

+ + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 15, "Extensions SmartyPants Quotes"); + TestParser.TestSpec("This is \"a *text\" with an emphasis*", "

This is “a text” with an emphasis

", "smartypants"); + } + } + // ## SmartyPants Separators + [TestFixture] + public partial class TestExtensionsSmartyPantsSeparators + { + [Test] + public void Example016() + { + // Example 16 + // Section: Extensions SmartyPants Separators // // The following CommonMark: // This is a -- text @@ -18514,18 +18748,18 @@ namespace Textamina.Markdig.Tests // Should be rendered as: //

This is a – text

- Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 4, "Extensions SmartyPants"); + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 16, "Extensions SmartyPants Separators"); TestParser.TestSpec("This is a -- text", "

This is a – text

", "smartypants"); } } [TestFixture] - public partial class TestExtensionsSmartyPants + public partial class TestExtensionsSmartyPantsSeparators { [Test] - public void Example005() + public void Example017() { - // Example 5 - // Section: Extensions SmartyPants + // Example 17 + // Section: Extensions SmartyPants Separators // // The following CommonMark: // This is a --- text @@ -18533,18 +18767,18 @@ namespace Textamina.Markdig.Tests // Should be rendered as: //

This is a — text

- Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 5, "Extensions SmartyPants"); + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 17, "Extensions SmartyPants Separators"); TestParser.TestSpec("This is a --- text", "

This is a — text

", "smartypants"); } } [TestFixture] - public partial class TestExtensionsSmartyPants + public partial class TestExtensionsSmartyPantsSeparators { [Test] - public void Example006() + public void Example018() { - // Example 6 - // Section: Extensions SmartyPants + // Example 18 + // Section: Extensions SmartyPants Separators // // The following CommonMark: // This is a en ellipsis... @@ -18552,7 +18786,7 @@ namespace Textamina.Markdig.Tests // Should be rendered as: //

This is a en ellipsis…

- Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 6, "Extensions SmartyPants"); + Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 18, "Extensions SmartyPants Separators"); TestParser.TestSpec("This is a en ellipsis...", "

This is a en ellipsis…

", "smartypants"); } } diff --git a/src/Textamina.Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs b/src/Textamina.Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs new file mode 100644 index 00000000..15bf113d --- /dev/null +++ b/src/Textamina.Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs @@ -0,0 +1,36 @@ +// 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 Textamina.Markdig.Renderers; +using Textamina.Markdig.Renderers.Html; + +namespace Textamina.Markdig.Extensions.SmartyPants +{ + /// + /// A HTML renderer for a . + /// + /// + public class HtmlSmartyPantRenderer : HtmlObjectRenderer + { + private static readonly SmartyPantOptions DefaultOptions = new SmartyPantOptions(); + + private readonly SmartyPantOptions options; + + public HtmlSmartyPantRenderer(SmartyPantOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + this.options = options; + } + + protected override void Write(HtmlRenderer renderer, SmartyPant obj) + { + string text; + if (!options.Mapping.TryGetValue(obj.Type, out text)) + { + DefaultOptions.Mapping.TryGetValue(obj.Type, out text); + } + renderer.Write(text); + } + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPant.cs b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPant.cs new file mode 100644 index 00000000..2b003868 --- /dev/null +++ b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPant.cs @@ -0,0 +1,45 @@ +// 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.Diagnostics; +using Textamina.Markdig.Syntax.Inlines; + +namespace Textamina.Markdig.Extensions.SmartyPants +{ + /// + /// An inline for SmartyPant. + /// + [DebuggerDisplay("SmartyPant {ToLiteral()}")] + public class SmartyPant : LeafInline + { + public char OpeningCharacter { get; set; } + + public SmartyPantType Type { get; set; } + + public string ToLiteral() + { + switch (Type) + { + case SmartyPantType.Quote: + case SmartyPantType.LeftQuote: + case SmartyPantType.RightQuote: + return "'"; + case SmartyPantType.DoubleQuote: + return "\""; + case SmartyPantType.LeftDoubleQuote: + return OpeningCharacter == '`' ? "``" : "\""; + case SmartyPantType.RightDoubleQuote: + return OpeningCharacter == '\'' ? "''" : "\""; + case SmartyPantType.Dash2: + return "--"; + case SmartyPantType.Dash3: + return "--"; + case SmartyPantType.LeftAngleQuote: + return "<<"; + case SmartyPantType.RightAngleQuote: + return ">>"; + } + return OpeningCharacter != 0 ? OpeningCharacter.ToString() : string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantOptions.cs b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantOptions.cs new file mode 100644 index 00000000..e111df6d --- /dev/null +++ b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantOptions.cs @@ -0,0 +1,33 @@ +// 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; + +namespace Textamina.Markdig.Extensions.SmartyPants +{ + /// + /// The options used for . + /// + public class SmartyPantOptions + { + public SmartyPantOptions() + { + Mapping = new Dictionary() + { + {SmartyPantType.Quote, "'"}, + {SmartyPantType.DoubleQuote, "\""}, + {SmartyPantType.LeftQuote, "‘"}, + {SmartyPantType.RightQuote, "’"}, + {SmartyPantType.LeftDoubleQuote, "“"}, + {SmartyPantType.RightDoubleQuote, "”"}, + {SmartyPantType.LeftAngleQuote, "«"}, + {SmartyPantType.RightAngleQuote, "»"}, + {SmartyPantType.Ellipsis, "…"}, + {SmartyPantType.Dash2, "–"}, + {SmartyPantType.Dash3, "—"}, + }; + } + + public Dictionary Mapping { get; } + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantType.cs b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantType.cs new file mode 100644 index 00000000..a3670629 --- /dev/null +++ b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantType.cs @@ -0,0 +1,33 @@ +// 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. +namespace Textamina.Markdig.Extensions.SmartyPants +{ + /// + /// Types of a . + /// + public enum SmartyPantType + { + Quote = 1, + + LeftQuote, + + RightQuote, + + DoubleQuote, + + LeftDoubleQuote, + + RightDoubleQuote, + + LeftAngleQuote, + + RightAngleQuote, + + Ellipsis, + + Dash2, + + Dash3, + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs index 592056aa..ebc322fd 100644 --- a/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs +++ b/src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantsExtension.cs @@ -2,17 +2,14 @@ // 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.Collections.Generic; -using Textamina.Markdig.Helpers; -using Textamina.Markdig.Parsers; using Textamina.Markdig.Parsers.Inlines; using Textamina.Markdig.Renderers; -using Textamina.Markdig.Renderers.Html; -using Textamina.Markdig.Syntax.Inlines; namespace Textamina.Markdig.Extensions.SmartyPants { + /// + /// Extension to enable SmartyPants. + /// public class SmartyPantsExtension : IMarkdownExtension { public SmartyPantsExtension(SmartyPantOptions options) @@ -40,319 +37,4 @@ namespace Textamina.Markdig.Extensions.SmartyPants } } } - - public enum SmartyPantType - { - Quote, - - LeftQuote, - - RightQuote, - - DoubleQuote, - - LeftDoubleQuote, - - RightDoubleQuote, - - LeftAngleQuote, - - RightAngleQuote, - - Ellipsis, - - Dash2, - - Dash3, - } - - public class SmartyPantOptions - { - public SmartyPantOptions() - { - Mapping = new Dictionary() - { - {SmartyPantType.Quote, "'"}, - {SmartyPantType.DoubleQuote, "\""}, - {SmartyPantType.LeftQuote, "‘"}, - {SmartyPantType.RightQuote, "’"}, - {SmartyPantType.LeftDoubleQuote, "“"}, - {SmartyPantType.RightDoubleQuote, "”"}, - {SmartyPantType.LeftAngleQuote, "«"}, - {SmartyPantType.RightAngleQuote, "»"}, - {SmartyPantType.Ellipsis, "…"}, - {SmartyPantType.Dash2, "–"}, - {SmartyPantType.Dash3, "—"}, - }; - } - - public Dictionary Mapping { get; } - } - - public class HtmlSmartyPantRenderer : HtmlObjectRenderer - { - private static readonly SmartyPantOptions DefaultOptions = new SmartyPantOptions(); - - private readonly SmartyPantOptions options; - - public HtmlSmartyPantRenderer(SmartyPantOptions options) - { - if (options == null) throw new ArgumentNullException(nameof(options)); - this.options = options; - } - - protected override void Write(HtmlRenderer renderer, SmartyPant obj) - { - string text; - if (!options.Mapping.TryGetValue(obj.Type, out text)) - { - DefaultOptions.Mapping.TryGetValue(obj.Type, out text); - } - renderer.Write(text); - } - } - - public class SmartyPant : LeafInline - { - public char OpeningCharacter { get; set; } - - public SmartyPantType Type { get; set; } - } - - public class SmaryPantsInlineParser : InlineParser - { - public SmaryPantsInlineParser() - { - OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'}; - } - - 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' - // -- – – 'ndash' - // --- — — 'mdash' - - var pc = slice.PeekCharExtra(-1); - var c = slice.CurrentChar; - var openingChar = c; - - // undefined first - var type = (SmartyPantType) 0; - - switch (c) - { - case '\'': - type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines - if (slice.PeekChar(1) == '\'') - { - slice.NextChar(); - type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines - } - break; - case '"': - type = SmartyPantType.DoubleQuote; - break; - case '<': - if (slice.NextChar() == '<') - { - type = SmartyPantType.LeftAngleQuote; - } - break; - case '>': - if (slice.NextChar() == '>') - { - type = SmartyPantType.RightAngleQuote; - } - break; - case '.': - if (slice.NextChar() == '.' && slice.NextChar() == '.') - { - type = SmartyPantType.Ellipsis; - } - break; - case '-': - if (slice.NextChar() == '-') - { - type = SmartyPantType.Dash2; - if (slice.PeekChar(1) == '-') - { - slice.NextChar(); - type = SmartyPantType.Dash3; - } - } - break; - } - - // If it is not matched, early exit - if (type == 0) - { - return false; - } - - // Skip char - c = slice.NextChar(); - - bool canOpen; - bool canClose; - CharHelper.CheckOpenCloseDelimiter(pc, c, false, out canOpen, out canClose); - - bool postProcess = false; - - switch (type) - { - case SmartyPantType.Quote: - postProcess = true; - if (canOpen && !canClose) - { - type = SmartyPantType.LeftQuote; - } - else if (!canOpen && canClose) - { - type = SmartyPantType.RightQuote; - } - else - { - return false; - } - break; - case SmartyPantType.DoubleQuote: - postProcess = true; - if (canOpen && !canClose) - { - type = SmartyPantType.LeftDoubleQuote; - } - else if (!canOpen && canClose) - { - type = SmartyPantType.RightDoubleQuote; - } - else - { - return false; - } - break; - case SmartyPantType.LeftAngleQuote: - postProcess = true; - if (!canOpen || canClose) - { - return false; - } - break; - case SmartyPantType.RightAngleQuote: - postProcess = true; - if (canOpen || !canClose) - { - return false; - } - break; - case SmartyPantType.Dash2: - case SmartyPantType.Dash3: - if (!canOpen && !canClose) - { - return false; - } - break; - case SmartyPantType.Ellipsis: - if (canOpen || !canClose) - { - return false; - } - break; - } - - // Create the SmartyPant inline - var pant = new SmartyPant() - { - OpeningCharacter = openingChar, - Type = type - }; - - // We will check in a post-process step for balanaced open/close quotes - if (postProcess) - { - var quotePants = processor.ParserStates[Index] as List; - if (quotePants == null) - { - processor.ParserStates[Index] = quotePants = new List(); - } - // Register only if we don't have yet any quotes - if (quotePants.Count == 0) - { - processor.Block.ProcessInlinesEnd += BlockOnProcessInlinesEnd; - } - quotePants.Add(pant); - } - - processor.Inline = pant; - return true; - } - - private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline inline) - { - processor.Block.ProcessInlinesEnd -= BlockOnProcessInlinesEnd; - - var pants = (List) processor.ParserStates[Index]; - - // We only change quote into left or right quotes if we find proper balancing - SmartyPant previousSimpleQuote = null; - SmartyPant previousDoubleQuote = null; - SmartyPant previousAngleQuote = null; - for (int i = 0; i < pants.Count; i++) - { - var quote = pants[i]; - - if (quote.Type == SmartyPantType.LeftQuote || quote.Type == SmartyPantType.RightQuote) - { - HandleLeftRight(quote, ref previousSimpleQuote, SmartyPantType.LeftQuote, SmartyPantType.RightQuote, - "'", "'"); - } - else if (quote.Type == SmartyPantType.LeftDoubleQuote || quote.Type == SmartyPantType.RightDoubleQuote) - { - var isRegularDoubleQuote = quote.OpeningCharacter == '"'; - - HandleLeftRight(quote, ref previousDoubleQuote, SmartyPantType.LeftDoubleQuote, SmartyPantType.RightDoubleQuote, - isRegularDoubleQuote ? "\"" : "``", isRegularDoubleQuote ? "\"" : "''"); - } - else if (quote.Type == SmartyPantType.LeftAngleQuote || quote.Type == SmartyPantType.RightAngleQuote) - { - HandleLeftRight(quote, ref previousAngleQuote, SmartyPantType.LeftAngleQuote, SmartyPantType.RightAngleQuote, - "<<", ">>"); - } - } - - pants.Clear(); - } - - private void HandleLeftRight(SmartyPant quote, ref SmartyPant previousQuote, SmartyPantType left, SmartyPantType right, string leftText, string rightText) - { - if (previousQuote == null) - { - if (quote.Type == left) - { - previousQuote = quote; - } - else - { - quote.ReplaceBy(new LiteralInline(rightText)); - } - } - else - { - if (quote.Type == left) - { - previousQuote.ReplaceBy(new LiteralInline(leftText)); - previousQuote = quote; - } - else - { - previousQuote = null; - } - } - } - } } \ No newline at end of file diff --git a/src/Textamina.Markdig/Extensions/SmartyPants/SmaryPantsInlineParser.cs b/src/Textamina.Markdig/Extensions/SmartyPants/SmaryPantsInlineParser.cs new file mode 100644 index 00000000..aff6cc73 --- /dev/null +++ b/src/Textamina.Markdig/Extensions/SmartyPants/SmaryPantsInlineParser.cs @@ -0,0 +1,272 @@ +// 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 Textamina.Markdig.Helpers; +using Textamina.Markdig.Parsers; +using Textamina.Markdig.Syntax.Inlines; + +namespace Textamina.Markdig.Extensions.SmartyPants +{ + /// + /// The inline parser for SmartyPants. + /// + public class SmaryPantsInlineParser : InlineParser + { + public SmaryPantsInlineParser() + { + OpeningCharacters = new[] {'`', '\'', '"', '<', '>', '.', '-'}; + } + + 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' + // -- – – 'ndash' + // --- — — 'mdash' + + var pc = slice.PeekCharExtra(-1); + var c = slice.CurrentChar; + var openingChar = c; + + // undefined first + var type = (SmartyPantType) 0; + + switch (c) + { + case '`': + if (slice.PeekChar(1) == '`') + { + slice.NextChar(); + type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines + } + break; + case '\'': + type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines + if (slice.PeekChar(1) == '\'') + { + slice.NextChar(); + type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines + } + break; + case '"': + type = SmartyPantType.DoubleQuote; + break; + case '<': + if (slice.NextChar() == '<') + { + type = SmartyPantType.LeftAngleQuote; + } + break; + case '>': + if (slice.NextChar() == '>') + { + type = SmartyPantType.RightAngleQuote; + } + break; + case '.': + if (slice.NextChar() == '.' && slice.NextChar() == '.') + { + type = SmartyPantType.Ellipsis; + } + break; + case '-': + if (slice.NextChar() == '-') + { + type = SmartyPantType.Dash2; + if (slice.PeekChar(1) == '-') + { + slice.NextChar(); + type = SmartyPantType.Dash3; + } + } + break; + } + + // If it is not matched, early exit + if (type == 0) + { + return false; + } + + // Skip char + c = slice.NextChar(); + + bool canOpen; + bool canClose; + CharHelper.CheckOpenCloseDelimiter(pc, c, false, out canOpen, out canClose); + + bool postProcess = false; + + switch (type) + { + case SmartyPantType.Quote: + postProcess = true; + if (canOpen && !canClose) + { + type = SmartyPantType.LeftQuote; + } + else if (!canOpen && canClose) + { + type = SmartyPantType.RightQuote; + } + else + { + return false; + } + break; + case SmartyPantType.DoubleQuote: + postProcess = true; + if (canOpen && !canClose) + { + type = SmartyPantType.LeftDoubleQuote; + } + else if (!canOpen && canClose) + { + type = SmartyPantType.RightDoubleQuote; + } + else + { + return false; + } + break; + case SmartyPantType.LeftAngleQuote: + postProcess = true; + if (!canOpen || canClose) + { + return false; + } + break; + case SmartyPantType.RightAngleQuote: + postProcess = true; + if (canOpen || !canClose) + { + return false; + } + break; + case SmartyPantType.Ellipsis: + if (canOpen || !canClose) + { + return false; + } + break; + } + + // Create the SmartyPant inline + var pant = new SmartyPant() + { + OpeningCharacter = openingChar, + Type = type + }; + + // We will check in a post-process step for balanaced open/close quotes + if (postProcess) + { + var quotePants = processor.ParserStates[Index] as List; + if (quotePants == null) + { + processor.ParserStates[Index] = quotePants = new List(); + } + // Register only if we don't have yet any quotes + if (quotePants.Count == 0) + { + processor.Block.ProcessInlinesEnd += BlockOnProcessInlinesEnd; + } + quotePants.Add(pant); + } + + processor.Inline = pant; + return true; + } + + private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline inline) + { + processor.Block.ProcessInlinesEnd -= BlockOnProcessInlinesEnd; + + var pants = (List) processor.ParserStates[Index]; + + // We only change quote into left or right quotes if we find proper balancing + var previousIndices = new int[3] {-1, -1, -1}; + + for (int i = 0; i < pants.Count; i++) + { + var quote = pants[i]; + + int currentTypeIndex = -1; + SmartyPantType expectedLeftQuote = 0; + SmartyPantType expectedRightQuote = 0; + + if (quote.Type == SmartyPantType.LeftQuote || quote.Type == SmartyPantType.RightQuote) + { + currentTypeIndex = 0; + expectedLeftQuote = SmartyPantType.LeftQuote; + expectedRightQuote = SmartyPantType.RightQuote; + } + else if (quote.Type == SmartyPantType.LeftDoubleQuote || quote.Type == SmartyPantType.RightDoubleQuote) + { + currentTypeIndex = 1; + expectedLeftQuote = SmartyPantType.LeftDoubleQuote; + expectedRightQuote = SmartyPantType.RightDoubleQuote; + } + else if (quote.Type == SmartyPantType.LeftAngleQuote || quote.Type == SmartyPantType.RightAngleQuote) + { + currentTypeIndex = 2; + expectedLeftQuote = SmartyPantType.LeftAngleQuote; + expectedRightQuote = SmartyPantType.RightAngleQuote; + } + + if (currentTypeIndex < 0) + { + continue; + } + + int previousIndex = previousIndices[currentTypeIndex]; + var previousQuote = previousIndex >= 0 ? pants[previousIndex] : null; + if (previousQuote == null) + { + if (quote.Type == expectedLeftQuote) + { + previousIndices[currentTypeIndex] = i; + } + } + else + { + if (quote.Type == expectedRightQuote) + { + // Replace all intermediate unmatched left or right SmartyPants to there literal equivalent + pants.RemoveAt(i); + i--; + for (int j = i; j > previousIndex; j--) + { + var toReplace = pants[j]; + pants.RemoveAt(j); + toReplace.ReplaceBy(new LiteralInline(toReplace.ToLiteral())); + i--; + } + + // If we matched, we remove left/right quotes from the list + pants.RemoveAt(previousIndex); + previousIndices[currentTypeIndex] = -1; + } + else + { + previousIndices[currentTypeIndex] = i; + } + } + } + + // If we have any quotes lefts, replace them by there literal equivalent + foreach (var quote in pants) + { + quote.ReplaceBy(new LiteralInline(quote.ToLiteral())); + } + + pants.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Textamina.Markdig/MarkdownExtensions.cs b/src/Textamina.Markdig/MarkdownExtensions.cs index 60b4982e..a1164622 100644 --- a/src/Textamina.Markdig/MarkdownExtensions.cs +++ b/src/Textamina.Markdig/MarkdownExtensions.cs @@ -45,6 +45,7 @@ namespace Textamina.Markdig .UseMath() .UseBootstrap() .UseMedia() + .UseSmartyPants() .UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers }