Finish SmartyPants extension (will need comments)

This commit is contained in:
Alexandre Mutel
2016-03-29 07:19:05 +09:00
parent 6800bf2ea9
commit f31a96a3c9
9 changed files with 767 additions and 347 deletions

View File

@@ -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 <<text>>
<p>This is a &laquo;text&raquo;</p>
````````````````````````````````
Unbalanced quotes are not changed:
```````````````````````````````` example
This is a "text
.
<p>This is a &quot;text</p>
````````````````````````````````
```````````````````````````````` example
This is a 'text
.
<p>This is a 'text</p>
````````````````````````````````
```````````````````````````````` example
This is a <<text
.
<p>This is a &lt;&lt;text</p>
````````````````````````````````
Unbalanced quotes inside other quotes are not changed:
```````````````````````````````` example
This is a "text 'with" a another text'
.
<p>This is a &ldquo;text 'with&rdquo; a another text'</p>
````````````````````````````````
```````````````````````````````` example
This is a 'text <<with' a another text>>
.
<p>This is a &lsquo;text &lt;&lt;with&rsquo; a another text&gt;&gt;</p>
````````````````````````````````
```````````````````````````````` example
This is a <<text 'with>> a another text'
.
<p>This is a &laquo;text 'with&raquo; a another text'</p>
````````````````````````````````
Quotes requires to have the same rules than emphasis `_` regarding left/right frankling rules:
```````````````````````````````` example
It's not quotes'
.
<p>It's not quotes'</p>
````````````````````````````````
```````````````````````````````` example
They are ' not matching quotes '
.
<p>They are ' not matching quotes '</p>
````````````````````````````````
```````````````````````````````` example
They are' not matching 'quotes
.
<p>They are' not matching 'quotes</p>
````````````````````````````````
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''
.
<p>This is &ldquo;a double quote&rdquo;</p>
````````````````````````````````
```````````````````````````````` example
This is ``a code span''``
.
<p>This is <code>a code span''</code></p>
````````````````````````````````
An emphasis starting inside left/right quotes will span over the right quote:
```````````````````````````````` example
This is "a *text" with an emphasis*
.
<p>This is &ldquo;a <em>text&rdquo; with an emphasis</em></p>
````````````````````````````````
## SmartyPants Separators
```````````````````````````````` example
This is a -- text
.

View File

@@ -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:
// <p>This is a &ldquo;text&rdquo;</p>
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\"", "<p>This is a &ldquo;text&rdquo;</p>", "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:
// <p>This is a &lsquo;text&rsquo;</p>
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'", "<p>This is a &lsquo;text&rsquo;</p>", "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 <<text>>
//
// Should be rendered as:
// <p>This is a &ldquo;text&rdquo;</p>
// <p>This is a &laquo;text&raquo;</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 3, "Extensions SmartyPants");
TestParser.TestSpec("This is a <<text>>", "<p>This is a &ldquo;text&rdquo;</p>", "smartypants");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 3, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a <<text>>", "<p>This is a &laquo;text&raquo;</p>", "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:
// <p>This is a &quot;text</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 4, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a \"text", "<p>This is a &quot;text</p>", "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:
// <p>This is a 'text</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 5, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a 'text", "<p>This is a 'text</p>", "smartypants");
}
}
[TestFixture]
public partial class TestExtensionsSmartyPantsQuotes
{
[Test]
public void Example006()
{
// Example 6
// Section: Extensions SmartyPants Quotes
//
// The following CommonMark:
// This is a <<text
//
// Should be rendered as:
// <p>This is a &lt;&lt;text</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 6, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a <<text", "<p>This is a &lt;&lt;text</p>", "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:
// <p>This is a &ldquo;text 'with&rdquo; a another text'</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 7, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a \"text 'with\" a another text'", "<p>This is a &ldquo;text 'with&rdquo; a another text'</p>", "smartypants");
}
}
[TestFixture]
public partial class TestExtensionsSmartyPantsQuotes
{
[Test]
public void Example008()
{
// Example 8
// Section: Extensions SmartyPants Quotes
//
// The following CommonMark:
// This is a 'text <<with' a another text>>
//
// Should be rendered as:
// <p>This is a &lsquo;text &lt;&lt;with&rsquo; a another text&gt;&gt;</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 8, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a 'text <<with' a another text>>", "<p>This is a &lsquo;text &lt;&lt;with&rsquo; a another text&gt;&gt;</p>", "smartypants");
}
}
[TestFixture]
public partial class TestExtensionsSmartyPantsQuotes
{
[Test]
public void Example009()
{
// Example 9
// Section: Extensions SmartyPants Quotes
//
// The following CommonMark:
// This is a <<text 'with>> a another text'
//
// Should be rendered as:
// <p>This is a &laquo;text 'with&raquo; a another text'</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 9, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is a <<text 'with>> a another text'", "<p>This is a &laquo;text 'with&raquo; a another text'</p>", "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:
// <p>It's not quotes'</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 10, "Extensions SmartyPants Quotes");
TestParser.TestSpec("It's not quotes'", "<p>It's not quotes'</p>", "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:
// <p>They are ' not matching quotes '</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 11, "Extensions SmartyPants Quotes");
TestParser.TestSpec("They are ' not matching quotes '", "<p>They are ' not matching quotes '</p>", "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:
// <p>They are' not matching 'quotes</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 12, "Extensions SmartyPants Quotes");
TestParser.TestSpec("They are' not matching 'quotes", "<p>They are' not matching 'quotes</p>", "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:
// <p>This is &ldquo;a double quote&rdquo;</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 13, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is ``a double quote''", "<p>This is &ldquo;a double quote&rdquo;</p>", "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:
// <p>This is <code>a code span''</code></p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 14, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is ``a code span''`` ", "<p>This is <code>a code span''</code></p>", "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:
// <p>This is &ldquo;a <em>text&rdquo; with an emphasis</em></p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 15, "Extensions SmartyPants Quotes");
TestParser.TestSpec("This is \"a *text\" with an emphasis*", "<p>This is &ldquo;a <em>text&rdquo; with an emphasis</em></p>", "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:
// <p>This is a &ndash; text</p>
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", "<p>This is a &ndash; text</p>", "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:
// <p>This is a &mdash; text</p>
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", "<p>This is a &mdash; text</p>", "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:
// <p>This is a en ellipsis&hellip;</p>
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...", "<p>This is a en ellipsis&hellip;</p>", "smartypants");
}
}

View File

@@ -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
{
/// <summary>
/// A HTML renderer for a <see cref="SmartyPant"/>.
/// </summary>
/// <seealso cref="Textamina.Markdig.Renderers.Html.HtmlObjectRenderer{SmartyPant}" />
public class HtmlSmartyPantRenderer : HtmlObjectRenderer<SmartyPant>
{
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);
}
}
}

View File

@@ -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
{
/// <summary>
/// An inline for SmartyPant.
/// </summary>
[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;
}
}
}

View File

@@ -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
{
/// <summary>
/// The options used for <see cref="SmartyPantsExtension"/>.
/// </summary>
public class SmartyPantOptions
{
public SmartyPantOptions()
{
Mapping = new Dictionary<SmartyPantType, string>()
{
{SmartyPantType.Quote, "'"},
{SmartyPantType.DoubleQuote, "\""},
{SmartyPantType.LeftQuote, "&lsquo;"},
{SmartyPantType.RightQuote, "&rsquo;"},
{SmartyPantType.LeftDoubleQuote, "&ldquo;"},
{SmartyPantType.RightDoubleQuote, "&rdquo;"},
{SmartyPantType.LeftAngleQuote, "&laquo;"},
{SmartyPantType.RightAngleQuote, "&raquo;"},
{SmartyPantType.Ellipsis, "&hellip;"},
{SmartyPantType.Dash2, "&ndash;"},
{SmartyPantType.Dash3, "&mdash;"},
};
}
public Dictionary<SmartyPantType, string> Mapping { get; }
}
}

View File

@@ -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
{
/// <summary>
/// Types of a <see cref="SmartyPant"/>.
/// </summary>
public enum SmartyPantType
{
Quote = 1,
LeftQuote,
RightQuote,
DoubleQuote,
LeftDoubleQuote,
RightDoubleQuote,
LeftAngleQuote,
RightAngleQuote,
Ellipsis,
Dash2,
Dash3,
}
}

View File

@@ -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
{
/// <summary>
/// Extension to enable SmartyPants.
/// </summary>
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, string>()
{
{SmartyPantType.Quote, "'"},
{SmartyPantType.DoubleQuote, "\""},
{SmartyPantType.LeftQuote, "&lsquo;"},
{SmartyPantType.RightQuote, "&rsquo;"},
{SmartyPantType.LeftDoubleQuote, "&ldquo;"},
{SmartyPantType.RightDoubleQuote, "&rdquo;"},
{SmartyPantType.LeftAngleQuote, "&laquo;"},
{SmartyPantType.RightAngleQuote, "&raquo;"},
{SmartyPantType.Ellipsis, "&hellip;"},
{SmartyPantType.Dash2, "&ndash;"},
{SmartyPantType.Dash3, "&mdash;"},
};
}
public Dictionary<SmartyPantType, string> Mapping { get; }
}
public class HtmlSmartyPantRenderer : HtmlObjectRenderer<SmartyPant>
{
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:
//
// ' ‘ ’ &lsquo; &rsquo; 'left-single-quote', 'right-single-quote'
// '' “ ” &ldquo; &rdquo; 'left-double-quote', 'right-double-quote'
// " “ ” &ldquo; &rdquo; 'left-double-quote', 'right-double-quote'
// << >> « » &laquo; &raquo; 'left-angle-quote', 'right-angle-quote'
// ... … &hellip; 'ellipsis'
// -- – &ndash; 'ndash'
// --- — &mdash; '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<SmartyPant>;
if (quotePants == null)
{
processor.ParserStates[Index] = quotePants = new List<SmartyPant>();
}
// 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<SmartyPant>) 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;
}
}
}
}
}

View File

@@ -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
{
/// <summary>
/// The inline parser for SmartyPants.
/// </summary>
public class SmaryPantsInlineParser : InlineParser
{
public SmaryPantsInlineParser()
{
OpeningCharacters = new[] {'`', '\'', '"', '<', '>', '.', '-'};
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// We are matching the following characters:
//
// ' ‘ ’ &lsquo; &rsquo; 'left-single-quote', 'right-single-quote'
// '' “ ” &ldquo; &rdquo; 'left-double-quote', 'right-double-quote'
// " “ ” &ldquo; &rdquo; 'left-double-quote', 'right-double-quote'
// << >> « » &laquo; &raquo; 'left-angle-quote', 'right-angle-quote'
// ... … &hellip; 'ellipsis'
// -- – &ndash; 'ndash'
// --- — &mdash; '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<SmartyPant>;
if (quotePants == null)
{
processor.ParserStates[Index] = quotePants = new List<SmartyPant>();
}
// 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<SmartyPant>) 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();
}
}
}

View File

@@ -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
}