From f31a96a3c949ed1a53debe2101e908fef6fd893d Mon Sep 17 00:00:00 2001
From: Alexandre Mutel
Date: Tue, 29 Mar 2016 07:19:05 +0900
Subject: [PATCH] Finish SmartyPants extension (will need comments)
---
.../Specs/SmartyPantsSpecs.md | 86 ++++-
src/Textamina.Markdig.Tests/Specs/Specs.cs | 284 +++++++++++++--
.../SmartyPants/HtmlSmartyPantRenderer.cs | 36 ++
.../Extensions/SmartyPants/SmartyPant.cs | 45 +++
.../SmartyPants/SmartyPantOptions.cs | 33 ++
.../Extensions/SmartyPants/SmartyPantType.cs | 33 ++
.../SmartyPants/SmartyPantsExtension.cs | 324 +-----------------
.../SmartyPants/SmaryPantsInlineParser.cs | 272 +++++++++++++++
src/Textamina.Markdig/MarkdownExtensions.cs | 1 +
9 files changed, 767 insertions(+), 347 deletions(-)
create mode 100644 src/Textamina.Markdig/Extensions/SmartyPants/HtmlSmartyPantRenderer.cs
create mode 100644 src/Textamina.Markdig/Extensions/SmartyPants/SmartyPant.cs
create mode 100644 src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantOptions.cs
create mode 100644 src/Textamina.Markdig/Extensions/SmartyPants/SmartyPantType.cs
create mode 100644 src/Textamina.Markdig/Extensions/SmartyPants/SmaryPantsInlineParser.cs
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
}