Compare commits

...

58 Commits

Author SHA1 Message Date
Alexandre Mutel
dd6418d108 Bump to 0.18.1 2020-01-21 08:30:32 +01:00
Alexandre Mutel
7875e4bce9 Merge pull request #387 from MihaZupan/descendants-api
Add missing Descendants<T> api
2020-01-21 08:24:13 +01:00
Alexandre Mutel
2e1b1a1fdc Merge pull request #386 from mlaily/emoji-customization
Emojis and smileys customization
2020-01-21 08:23:48 +01:00
MihaZupan
fa2b157c1a Improve some CharHelper methods 2020-01-21 01:31:44 +01:00
MihaZupan
e4e6406546 Add missing Descendants<T> overload 2020-01-21 01:03:47 +01:00
Melvyn Laïly
1cff10270a Set the default emoji dictionaries capacity in their ctor 2020-01-16 09:12:05 +01:00
Melvyn Laïly
87023184cb Clarify emoji terminology (emoji "shortcode") 2020-01-16 09:12:04 +01:00
Melvyn Laïly
aecdf2192e Improve the emoji extension (code review remarks) 2020-01-16 09:12:04 +01:00
Melvyn Laïly
0a36382126 Update changelog 2020-01-16 09:12:04 +01:00
Melvyn Laïly
0057b368ec Add unit tests for custom emojis and smileys 2020-01-16 09:11:04 +01:00
Melvyn Laïly
3033284096 Re-allow emojis and smileys customization
(this feature was broken in #308)
2020-01-16 09:11:03 +01:00
Alexandre Mutel
9b1b791b18 Merge pull request #341 from OpportunityLiu/master
Add customizable media link; handle protocol-less media
2020-01-15 13:27:06 +01:00
Opportunity
2b7d205701 Merge branch 'master' into master 2020-01-15 19:35:17 +08:00
Opportunity
89b28659b1 Update src/Markdig/Extensions/MediaLinks/HostProviderBuilder.cs
Co-Authored-By: Alexandre Mutel <alexandre_mutel@live.com>
2020-01-15 19:32:37 +08:00
Alexandre Mutel
446b1bcc0d Bump to 0.18.0 2019-10-24 21:08:11 +02:00
Alexandre Mutel
ec7a4a6902 Create FUNDING.yml 2019-10-24 07:10:27 +02:00
Alexandre Mutel
07a77142f4 Merge pull request #377 from MihaZupan/allocation-reductions
10% time and 50% memory improvement
2019-10-15 23:06:31 +02:00
MihaZupan
3606f234b8 Update changelog 2019-10-15 15:09:34 +02:00
MihaZupan
616eed62bd Use try/finally instead of goto release in AutoLinkParser 2019-10-15 15:07:45 +02:00
MihaZupan
99f55e9ddc Resolve merge conflict 2019-10-15 14:58:03 +02:00
Alexandre Mutel
f8ab1cccc5 Merge pull request #375 from MihaZupan/balanced-link-brackets
Balanced link brackets
2019-10-15 13:46:31 +02:00
MihaZupan
f24067cd16 Update AppVeyor build image 2019-10-14 19:11:28 +02:00
MihaZupan
9af96ba2b4 Minor CharHelper optimizations 2019-10-14 19:03:15 +02:00
MihaZupan
c99f7dd96a Mark some struct methods as readonly 2019-10-14 18:39:40 +02:00
MihaZupan
bf28cbd33f Optimize ContainerBlock this[] accessor 2019-10-14 18:32:24 +02:00
MihaZupan
2040e23545 Correctly return IsEmpty for ICharIterator.TrimStart 2019-10-14 18:31:51 +02:00
MihaZupan
2761e36b6b Optimize StringSlice primitives 2019-10-14 18:28:32 +02:00
MihaZupan
891334134c Mark Readonly structs as Readonly 2019-10-14 13:38:21 +02:00
MihaZupan
0987fab6f2 Seal internal types 2019-10-14 13:33:24 +02:00
MihaZupan
ed5eea5e27 Cache List<char> in AutoLinkParser 2019-10-14 13:26:09 +02:00
MihaZupan
f73cbe4e76 Resize LineOffsets to sufficient Capacity before adding items 2019-10-14 13:11:07 +02:00
MihaZupan
aefad219cf Cache StringLine[]s in StringLineGroup with a custom ArrayPool 2019-10-14 13:04:27 +02:00
MihaZupan
76c3e88c58 Estimate LineCount from text Length to minimize List resizes 2019-10-14 12:58:03 +02:00
MihaZupan
afe4308e91 Cache HtmlRenderer on Pipeline for ToHtml(string, Pipeline) 2019-10-13 18:03:13 +02:00
MihaZupan
606556b692 Use Write(Span) on NetCore 2019-10-13 15:44:36 +02:00
MihaZupan
253be5c362 Cache HtmlRenderers in AutoIdentifier Extension 2019-10-13 15:17:33 +02:00
MihaZupan
33037d1034 Update changelog 2019-10-08 17:29:49 +02:00
MihaZupan
f16ee828db Fix link text balanced bracket matching 2019-10-08 17:27:50 +02:00
MihaZupan
a76305f39b Add tests for balanced brackets in link text
For issue #371
2019-10-08 17:26:16 +02:00
MihaZupan
f52a41e167 Cleanup LinkInline parsing code
No functional changes, readability only
Switch with a single case => if statement
Pattern matching, inline out variable declarations ...
2019-10-08 16:22:53 +02:00
Opportunity
4d172bf905 Merge branch 'master' into master 2019-10-01 21:31:02 +08:00
Opportunity
890b2cda2a Update MediaLinkExtension.cs 2019-09-30 21:09:44 +08:00
Alexandre Mutel
c818670919 Merge pull request #360 from MihaZupan/smarty-pants
Fix SmartyPants quote matching
2019-09-25 22:15:36 +02:00
Alexandre Mutel
a78a0b7016 Merge pull request #361 from MihaZupan/generic-attributes
Fix GenericAttributes matching of single-char values
2019-09-25 22:15:22 +02:00
Miha Zupan
6a0c9aeb47 Fix GenericAttributes matching of single-char values 2019-08-01 16:14:51 +02:00
Miha Zupan
b1cfcf2431 Add GenericAttributes test for #182 2019-08-01 16:13:37 +02:00
Miha Zupan
b411522a23 Increase EnsureSpecsAreUpToDate time leeway to 3min 2019-08-01 16:12:44 +02:00
Miha Zupan
1d2977d47b Fix SmartyPants quote matching 2019-08-01 15:28:20 +02:00
Miha Zupan
ee8c87c357 Add SmartyPants tests for #183 2019-08-01 15:26:04 +02:00
Alexandre Mutel
25959174d5 Merge pull request #357 from MihaZupan/master
Ignore backticks in GFM AutoLinks
2019-07-17 09:25:33 +02:00
Miha Zupan
033ddaf6a8 Ignore backticks in GFM AutoLinks 2019-07-16 10:21:52 +02:00
OpportunityLiu
8886b48634 add tests 2019-05-16 15:58:02 +08:00
OpportunityLiu
64cd8ec262 customize iframe class of each provider 2019-05-15 10:48:12 +08:00
OpportunityLiu
a1e19912a9 update changelog 2019-05-14 17:32:09 +08:00
OpportunityLiu
ca51967fb1 fix #135 2019-05-14 17:31:03 +08:00
OpportunityLiu
bb3a4f372c add change log 2019-05-14 16:07:14 +08:00
OpportunityLiu
1d6a464c5d add unit test 2019-05-14 15:59:05 +08:00
OpportunityLiu
0fe5c17a93 Add customizable media link 2019-05-14 15:30:15 +08:00
65 changed files with 3555 additions and 2643 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [xoofx]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,5 +1,5 @@
version: 10.0.{build}
image: Visual Studio 2017
image: Visual Studio 2019
configuration: Release
environment:
COVERALLS_REPO_TOKEN:

View File

@@ -1,5 +1,18 @@
# Changelog
## 0.18.1 (21 Jan 2020)
- Re-allow emojis and smileys customization, that was broken in [PR #308](https://github.com/lunet-io/markdig/pull/308) ([PR #386](https://github.com/lunet-io/markdig/pull/386))
- Add `IHostProvider` for medialink customization (#337), support protocol-less url (#135) ([(PR #341)](https://github.com/lunet-io/markdig/pull/341))
- Add missing Descendants<T> overload ([(PR #387)](https://github.com/lunet-io/markdig/pull/387))
## 0.18.0 (24 Oct 2019)
- Ignore backslashes in GFM AutoLinks ([(PR #357)](https://github.com/lunet-io/markdig/pull/357))
- Fix SmartyPants quote matching ([(PR #360)](https://github.com/lunet-io/markdig/pull/360))
- Fix generic attributes with values of length 1 ([(PR #361)](https://github.com/lunet-io/markdig/pull/361))
- Fix link text balanced bracket matching ([(PR #375)](https://github.com/lunet-io/markdig/pull/375))
- Improve overall performance and substantially reduce allocations ([(PR #377)](https://github.com/lunet-io/markdig/pull/377))
## 0.17.1 (04 July 2019)
- Fix regression when escaping HTML characters ([(PR #340)](https://github.com/lunet-io/markdig/pull/340))
- Update Emoji Dictionary ([(PR #346)](https://github.com/lunet-io/markdig/pull/346))

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Markdig.Extensions.AutoLinks;
using NUnit.Framework;
@@ -8,6 +9,55 @@ namespace Markdig.Tests
{
public class MiscTests
{
[TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508
[TestCase("link [foo][bar]")]
[TestCase("link [][foo][bar][]")]
[TestCase("link [][foo][bar][[]]")]
[TestCase("link [foo] [bar]")]
[TestCase("link [[foo] [] [bar] [[abc]def]]")]
[TestCase("[]")]
[TestCase("[ ]")]
[TestCase("[bar][]")]
[TestCase("[bar][ foo]")]
[TestCase("[bar][foo ][]")]
[TestCase("[bar][fo[ ]o ][][]")]
[TestCase("[a]b[c[d[e]f]g]h")]
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")]
[TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")]
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")]
public void LinkTextMayContainBalancedBrackets(string linkText)
{
string markdown = $"[{linkText}](/uri)";
string expected = $@"<p><a href=""/uri"">{linkText}</a></p>";
TestParser.TestSpec(markdown, expected);
// Make the link text unbalanced
foreach (var bracketIndex in linkText
.Select((c, i) => new Tuple<char, int>(c, i))
.Where(t => t.Item1 == '[' || t.Item1 == ']')
.Select(t => t.Item2))
{
string brokenLinkText = linkText.Remove(bracketIndex, 1);
markdown = $"[{brokenLinkText}](/uri)";
expected = $@"<p><a href=""/uri"">{brokenLinkText}</a></p>";
string actual = Markdown.ToHtml(markdown);
Assert.AreNotEqual(expected, actual);
}
}
[Test]
public void IsIssue356Corrected()
{
string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97";
string expected = @"<p><a href=""https://foo.bar/path/%5C#m4mv5W0GYKZpGvfA.97"">https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97</a></p>";
TestParser.TestSpec($"<{input}>", expected);
TestParser.TestSpec(input, expected, "autolinks|advanced");
}
[Test]
public void TestAltTextIsCorrectlyEscaped()
{

View File

@@ -1,4 +1,4 @@
// Generated: 2019-04-05 16:06:14
// Generated: 2019-05-15 02:46:55
// --------------------------------
// Abbreviations

View File

@@ -1,4 +1,4 @@
// Generated: 2019-04-15 05:20:50
// Generated: 2020-01-13 21:08:58
// --------------------------------
// Emoji
@@ -18,7 +18,7 @@ namespace Markdig.Tests.Specs.Emoji
//
// ## Emoji
//
// Emoji and smiley can be converted to their respective unicode characters:
// Emoji shortcodes and smileys can be converted to their respective unicode characters:
[Test]
public void ExtensionsEmoji_Example001()
{
@@ -52,7 +52,7 @@ namespace Markdig.Tests.Specs.Emoji
TestParser.TestSpec("These are not:) an emoji with a:) x:angry:x", "<p>These are not:) an emoji with a:) x:angry:x</p>", "emojis|advanced+emojis");
}
// Emoji can be followed by close punctuation (or any other characters):
// Emojis can be followed by close punctuation (or any other characters):
[Test]
public void ExtensionsEmoji_Example003()
{
@@ -69,7 +69,7 @@ namespace Markdig.Tests.Specs.Emoji
TestParser.TestSpec("We all need :), it makes us :muscle:. (and :ok_hand:).", "<p>We all need 😃, it makes us 💪. (and 👌).</p>", "emojis|advanced+emojis");
}
// Sentences can end with Emoji:
// Sentences can end with emojis:
[Test]
public void ExtensionsEmoji_Example004()
{

View File

@@ -4,7 +4,7 @@ This section describes the different extensions supported:
## Emoji
Emoji and smiley can be converted to their respective unicode characters:
Emoji shortcodes and smileys can be converted to their respective unicode characters:
```````````````````````````````` example
This is a test with a :) and a :angry: smiley
@@ -20,7 +20,7 @@ These are not:) an emoji with a:) x:angry:x
<p>These are not:) an emoji with a:) x:angry:x</p>
````````````````````````````````
Emoji can be followed by close punctuation (or any other characters):
Emojis can be followed by close punctuation (or any other characters):
```````````````````````````````` example
We all need :), it makes us :muscle:. (and :ok_hand:).
@@ -28,7 +28,7 @@ We all need :), it makes us :muscle:. (and :ok_hand:).
<p>We all need 😃, it makes us 💪. (and 👌).</p>
````````````````````````````````
Sentences can end with Emoji:
Sentences can end with emojis:
```````````````````````````````` example
This is a sentence :ok_hand:

View File

@@ -1,4 +1,4 @@
// Generated: 2019-04-05 16:06:14
// Generated: 2019-08-01 13:57:17
// --------------------------------
// Generic Attributes
@@ -79,5 +79,28 @@ namespace Markdig.Tests.Specs.GenericAttributes
Console.WriteLine("Example 2\nSection Extensions / Generic Attributes\n");
TestParser.TestSpec("{#fenced-id .fenced-class}\n~~~\nThis is a fenced with attached attributes\n~~~ ", "<pre><code id=\"fenced-id\" class=\"fenced-class\">This is a fenced with attached attributes\n</code></pre>", "attributes|advanced");
}
// Attribute values can be one character long
[Test]
public void ExtensionsGenericAttributes_Example003()
{
// Example 3
// Section: Extensions / Generic Attributes
//
// The following Markdown:
// [Foo](url){data-x=1}
//
// [Foo](url){data-x='1'}
//
// [Foo](url){data-x=11}
//
// Should be rendered as:
// <p><a href="url" data-x="1">Foo</a></p>
// <p><a href="url" data-x="1">Foo</a></p>
// <p><a href="url" data-x="11">Foo</a></p>
Console.WriteLine("Example 3\nSection Extensions / Generic Attributes\n");
TestParser.TestSpec("[Foo](url){data-x=1}\n\n[Foo](url){data-x='1'}\n\n[Foo](url){data-x=11}", "<p><a href=\"url\" data-x=\"1\">Foo</a></p>\n<p><a href=\"url\" data-x=\"1\">Foo</a></p>\n<p><a href=\"url\" data-x=\"11\">Foo</a></p>", "attributes|advanced");
}
}
}

View File

@@ -47,3 +47,17 @@ This is a fenced with attached attributes
<pre><code id="fenced-id" class="fenced-class">This is a fenced with attached attributes
</code></pre>
````````````````````````````````
Attribute values can be one character long
```````````````````````````````` example
[Foo](url){data-x=1}
[Foo](url){data-x='1'}
[Foo](url){data-x=11}
.
<p><a href="url" data-x="1">Foo</a></p>
<p><a href="url" data-x="1">Foo</a></p>
<p><a href="url" data-x="11">Foo</a></p>
````````````````````````````````

View File

@@ -1,4 +1,4 @@
// Generated: 2019-04-29 18:40:06
// Generated: 2019-05-15 02:46:20
// --------------------------------
// Media
@@ -47,19 +47,19 @@ namespace Markdig.Tests.Specs.Media
// ![ok.ru](https://ok.ru/video/26870090463)
//
// Should be rendered as:
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100&amp;rel=0" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed?listType=playlist&amp;list=PLC77007E23FF423C6" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100&amp;rel=0" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://www.youtube.com/embed?listType=playlist&amp;list=PLC77007E23FF423C6" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" class="vimeo" frameborder="0" allowfullscreen=""></iframe></p>
// <p><video width="500" height="281" controls=""><source type="video/mp4" src="https://sample.com/video.mp4"></source></video></p>
// <p><iframe src="https://music.yandex.ru/iframe/#track/4402274/411845/" width="500" height="281" frameborder="0"></iframe></p>
// <p><iframe src="https://ok.ru/videoembed/26870090463" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
// <p><iframe src="https://music.yandex.ru/iframe/#track/4402274/411845/" width="500" height="281" class="yandex" frameborder="0"></iframe></p>
// <p><iframe src="https://ok.ru/videoembed/26870090463" width="500" height="281" class="odnoklassniki" frameborder="0" allowfullscreen=""></iframe></p>
Console.WriteLine("Example 1\nSection Extensions / Media links\n");
TestParser.TestSpec("![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)\n\n![youtube.com with t](https://www.youtube.com/watch?v=mswPy5bt3TQ&t=100)\n\n![youtu.be](https://youtu.be/mswPy5bt3TQ)\n\n![youtu.be with t](https://youtu.be/mswPy5bt3TQ?t=100)\n\n![youtube.com/embed 1](https://www.youtube.com/embed/mswPy5bt3TQ?start=100&rel=0)\n \n![youtube.com/embed 2](https://www.youtube.com/embed?listType=playlist&list=PLC77007E23FF423C6)\n\n![vimeo](https://vimeo.com/8607834)\n\n![static mp4](https://sample.com/video.mp4)\n\n![yandex.ru](https://music.yandex.ru/album/411845/track/4402274)\n\n![ok.ru](https://ok.ru/video/26870090463)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ?start=100\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ?start=100\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ?start=100&amp;rel=0\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed?listType=playlist&amp;list=PLC77007E23FF423C6\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://player.vimeo.com/video/8607834\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"https://sample.com/video.mp4\"></source></video></p>\n<p><iframe src=\"https://music.yandex.ru/iframe/#track/4402274/411845/\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n<p><iframe src=\"https://ok.ru/videoembed/26870090463\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>", "medialinks|advanced+medialinks");
TestParser.TestSpec("![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)\n\n![youtube.com with t](https://www.youtube.com/watch?v=mswPy5bt3TQ&t=100)\n\n![youtu.be](https://youtu.be/mswPy5bt3TQ)\n\n![youtu.be with t](https://youtu.be/mswPy5bt3TQ?t=100)\n\n![youtube.com/embed 1](https://www.youtube.com/embed/mswPy5bt3TQ?start=100&rel=0)\n \n![youtube.com/embed 2](https://www.youtube.com/embed?listType=playlist&list=PLC77007E23FF423C6)\n\n![vimeo](https://vimeo.com/8607834)\n\n![static mp4](https://sample.com/video.mp4)\n\n![yandex.ru](https://music.yandex.ru/album/411845/track/4402274)\n\n![ok.ru](https://ok.ru/video/26870090463)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ?start=100\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ?start=100\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ?start=100&amp;rel=0\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://www.youtube.com/embed?listType=playlist&amp;list=PLC77007E23FF423C6\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><iframe src=\"https://player.vimeo.com/video/8607834\" width=\"500\" height=\"281\" class=\"vimeo\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"https://sample.com/video.mp4\"></source></video></p>\n<p><iframe src=\"https://music.yandex.ru/iframe/#track/4402274/411845/\" width=\"500\" height=\"281\" class=\"yandex\" frameborder=\"0\"></iframe></p>\n<p><iframe src=\"https://ok.ru/videoembed/26870090463\" width=\"500\" height=\"281\" class=\"odnoklassniki\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>", "medialinks|advanced+medialinks");
}
}
}

View File

@@ -27,14 +27,14 @@ Allows to embed audio/video links to popular website:
![ok.ru](https://ok.ru/video/26870090463)
.
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100&amp;rel=0" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed?listType=playlist&amp;list=PLC77007E23FF423C6" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ?start=100&amp;rel=0" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://www.youtube.com/embed?listType=playlist&amp;list=PLC77007E23FF423C6" width="500" height="281" class="youtube" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" class="vimeo" frameborder="0" allowfullscreen=""></iframe></p>
<p><video width="500" height="281" controls=""><source type="video/mp4" src="https://sample.com/video.mp4"></source></video></p>
<p><iframe src="https://music.yandex.ru/iframe/#track/4402274/411845/" width="500" height="281" frameborder="0"></iframe></p>
<p><iframe src="https://ok.ru/videoembed/26870090463" width="500" height="281" frameborder="0" allowfullscreen=""></iframe></p>
<p><iframe src="https://music.yandex.ru/iframe/#track/4402274/411845/" width="500" height="281" class="yandex" frameborder="0"></iframe></p>
<p><iframe src="https://ok.ru/videoembed/26870090463" width="500" height="281" class="odnoklassniki" frameborder="0" allowfullscreen=""></iframe></p>
````````````````````````````````

View File

@@ -1,4 +1,4 @@
// Generated: 2019-04-05 16:06:14
// Generated: 2019-08-01 12:33:23
// --------------------------------
// Smarty Pants
@@ -140,13 +140,13 @@ namespace Markdig.Tests.Specs.SmartyPants
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// This is a 'text <<with' a another text>>
// 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>
// <p>This is &lsquo;a &ldquo;text 'with&rdquo; a another text&rsquo;</p>
Console.WriteLine("Example 8\nSection Extensions / SmartyPants Quotes\n");
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>", "pipetables+smartypants|advanced+smartypants");
TestParser.TestSpec("This is 'a \"text 'with\" a another text'", "<p>This is &lsquo;a &ldquo;text 'with&rdquo; a another text&rsquo;</p>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
@@ -156,20 +156,36 @@ namespace Markdig.Tests.Specs.SmartyPants
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// 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 9\nSection Extensions / SmartyPants Quotes\n");
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>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
public void ExtensionsSmartyPantsQuotes_Example010()
{
// Example 10
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// 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 9\nSection Extensions / SmartyPants Quotes\n");
Console.WriteLine("Example 10\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("This is a <<text 'with>> a another text'", "<p>This is a &laquo;text 'with&raquo; a another text'</p>", "pipetables+smartypants|advanced+smartypants");
}
// Quotes requires to have the same rules than emphasis `_` regarding left/right frankling rules:
[Test]
public void ExtensionsSmartyPantsQuotes_Example010()
public void ExtensionsSmartyPantsQuotes_Example011()
{
// Example 10
// Example 11
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
@@ -178,24 +194,8 @@ namespace Markdig.Tests.Specs.SmartyPants
// Should be rendered as:
// <p>It's not quotes'</p>
Console.WriteLine("Example 10\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("It's not quotes'", "<p>It's not quotes'</p>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
public void ExtensionsSmartyPantsQuotes_Example011()
{
// Example 11
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// They are ' not matching quotes '
//
// Should be rendered as:
// <p>They are ' not matching quotes '</p>
Console.WriteLine("Example 11\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("They are ' not matching quotes '", "<p>They are ' not matching quotes '</p>", "pipetables+smartypants|advanced+smartypants");
TestParser.TestSpec("It's not quotes'", "<p>It's not quotes'</p>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
@@ -205,20 +205,36 @@ namespace Markdig.Tests.Specs.SmartyPants
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// They are ' not matching quotes '
//
// Should be rendered as:
// <p>They are ' not matching quotes '</p>
Console.WriteLine("Example 12\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("They are ' not matching quotes '", "<p>They are ' not matching quotes '</p>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
public void ExtensionsSmartyPantsQuotes_Example013()
{
// Example 13
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// They are' not matching 'quotes
//
// Should be rendered as:
// <p>They are' not matching 'quotes</p>
Console.WriteLine("Example 12\nSection Extensions / SmartyPants Quotes\n");
Console.WriteLine("Example 13\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("They are' not matching 'quotes", "<p>They are' not matching 'quotes</p>", "pipetables+smartypants|advanced+smartypants");
}
// An emphasis starting inside left/right quotes will span over the right quote:
[Test]
public void ExtensionsSmartyPantsQuotes_Example013()
public void ExtensionsSmartyPantsQuotes_Example014()
{
// Example 13
// Example 14
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
@@ -227,9 +243,26 @@ namespace Markdig.Tests.Specs.SmartyPants
// Should be rendered as:
// <p>This is &ldquo;a <em>text&rdquo; with an emphasis</em></p>
Console.WriteLine("Example 13\nSection Extensions / SmartyPants Quotes\n");
Console.WriteLine("Example 14\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("This is \"a *text\" with an emphasis*", "<p>This is &ldquo;a <em>text&rdquo; with an emphasis</em></p>", "pipetables+smartypants|advanced+smartypants");
}
// Multiple sets of quotes can be used
[Test]
public void ExtensionsSmartyPantsQuotes_Example015()
{
// Example 15
// Section: Extensions / SmartyPants Quotes
//
// The following Markdown:
// "aaa" "bbb" "ccc" "ddd"
//
// Should be rendered as:
// <p>&ldquo;aaa&rdquo; &ldquo;bbb&rdquo; &ldquo;ccc&rdquo; &ldquo;ddd&rdquo;</p>
Console.WriteLine("Example 15\nSection Extensions / SmartyPants Quotes\n");
TestParser.TestSpec("\"aaa\" \"bbb\" \"ccc\" \"ddd\"", "<p>&ldquo;aaa&rdquo; &ldquo;bbb&rdquo; &ldquo;ccc&rdquo; &ldquo;ddd&rdquo;</p>", "pipetables+smartypants|advanced+smartypants");
}
}
[TestFixture]
@@ -237,9 +270,9 @@ namespace Markdig.Tests.Specs.SmartyPants
{
// ## SmartyPants Separators
[Test]
public void ExtensionsSmartyPantsSeparators_Example014()
public void ExtensionsSmartyPantsSeparators_Example016()
{
// Example 14
// Example 16
// Section: Extensions / SmartyPants Separators
//
// The following Markdown:
@@ -248,14 +281,14 @@ namespace Markdig.Tests.Specs.SmartyPants
// Should be rendered as:
// <p>This is a &ndash; text</p>
Console.WriteLine("Example 14\nSection Extensions / SmartyPants Separators\n");
Console.WriteLine("Example 16\nSection Extensions / SmartyPants Separators\n");
TestParser.TestSpec("This is a -- text", "<p>This is a &ndash; text</p>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
public void ExtensionsSmartyPantsSeparators_Example015()
public void ExtensionsSmartyPantsSeparators_Example017()
{
// Example 15
// Example 17
// Section: Extensions / SmartyPants Separators
//
// The following Markdown:
@@ -264,14 +297,14 @@ namespace Markdig.Tests.Specs.SmartyPants
// Should be rendered as:
// <p>This is a &mdash; text</p>
Console.WriteLine("Example 15\nSection Extensions / SmartyPants Separators\n");
Console.WriteLine("Example 17\nSection Extensions / SmartyPants Separators\n");
TestParser.TestSpec("This is a --- text", "<p>This is a &mdash; text</p>", "pipetables+smartypants|advanced+smartypants");
}
[Test]
public void ExtensionsSmartyPantsSeparators_Example016()
public void ExtensionsSmartyPantsSeparators_Example018()
{
// Example 16
// Example 18
// Section: Extensions / SmartyPants Separators
//
// The following Markdown:
@@ -280,15 +313,15 @@ namespace Markdig.Tests.Specs.SmartyPants
// Should be rendered as:
// <p>This is a en ellipsis&hellip;</p>
Console.WriteLine("Example 16\nSection Extensions / SmartyPants Separators\n");
Console.WriteLine("Example 18\nSection Extensions / SmartyPants Separators\n");
TestParser.TestSpec("This is a en ellipsis...", "<p>This is a en ellipsis&hellip;</p>", "pipetables+smartypants|advanced+smartypants");
}
// Check that a smartypants are not breaking pipetable parsing:
[Test]
public void ExtensionsSmartyPantsSeparators_Example017()
public void ExtensionsSmartyPantsSeparators_Example019()
{
// Example 17
// Example 19
// Section: Extensions / SmartyPants Separators
//
// The following Markdown:
@@ -312,15 +345,15 @@ namespace Markdig.Tests.Specs.SmartyPants
// </tbody>
// </table>
Console.WriteLine("Example 17\nSection Extensions / SmartyPants Separators\n");
Console.WriteLine("Example 19\nSection Extensions / SmartyPants Separators\n");
TestParser.TestSpec("a | b\n-- | --\n0 | 1", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>", "pipetables+smartypants|advanced+smartypants");
}
// Check quotes and dash:
[Test]
public void ExtensionsSmartyPantsSeparators_Example018()
public void ExtensionsSmartyPantsSeparators_Example020()
{
// Example 18
// Example 20
// Section: Extensions / SmartyPants Separators
//
// The following Markdown:
@@ -329,7 +362,7 @@ namespace Markdig.Tests.Specs.SmartyPants
// Should be rendered as:
// <p>A &ldquo;quote&rdquo; with a &mdash;</p>
Console.WriteLine("Example 18\nSection Extensions / SmartyPants Separators\n");
Console.WriteLine("Example 20\nSection Extensions / SmartyPants Separators\n");
TestParser.TestSpec("A \"quote\" with a ---", "<p>A &ldquo;quote&rdquo; with a &mdash;</p>", "pipetables+smartypants|advanced+smartypants");
}
}

View File

@@ -52,6 +52,12 @@ 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 &lsquo;a &ldquo;text 'with&rdquo; a another text&rsquo;</p>
````````````````````````````````
```````````````````````````````` example
This is a 'text <<with' a another text>>
.
@@ -91,6 +97,14 @@ This is "a *text" with an emphasis*
<p>This is &ldquo;a <em>text&rdquo; with an emphasis</em></p>
````````````````````````````````
Multiple sets of quotes can be used
```````````````````````````````` example
"aaa" "bbb" "ccc" "ddd"
.
<p>&ldquo;aaa&rdquo; &ldquo;bbb&rdquo; &ldquo;ccc&rdquo; &ldquo;ddd&rdquo;</p>
````````````````````````````````
## SmartyPants Separators
```````````````````````````````` example

View File

@@ -0,0 +1,99 @@
using System.Collections.Generic;
using Markdig.Extensions.Emoji;
using NUnit.Framework;
namespace Markdig.Tests
{
[TestFixture]
public class TestCustomEmojis
{
[Test]
[TestCase(":smiley:", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>:confused:</p>\n")] // default emoji does not work
[TestCase(":/", "<p>:/</p>\n")] // default smiley does not work
public void TestCustomEmoji(string input, string expected)
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
emojiToUnicode[":smiley:"] = "♥";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(":testheart:", "<p>♥</p>\n")]
[TestCase("hello", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>:confused:</p>\n")] // default emoji does not work
[TestCase(":/", "<p>:/</p>\n")] // default smiley does not work
public void TestCustomSmiley(string input, string expected)
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
emojiToUnicode[":testheart:"] = "♥";
smileyToEmoji["hello"] = ":testheart:";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(":smiley:", "<p>♥</p>\n")]
[TestCase(":)", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>😕</p>\n")] // default emoji still works
[TestCase(":/", "<p>😕</p>\n")] // default smiley still works
public void TestOverrideDefaultWithCustomEmoji(string input, string expected)
{
var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode();
var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode();
emojiToUnicode[":smiley:"] = "♥";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(":testheart:", "<p>♥</p>\n")]
[TestCase("hello", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>😕</p>\n")] // default emoji still works
[TestCase(":/", "<p>😕</p>\n")] // default smiley still works
public void TestOverrideDefaultWithCustomSmiley(string input, string expected)
{
var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode();
var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode();
emojiToUnicode[":testheart:"] = "♥";
smileyToEmoji["hello"] = ":testheart:";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
}
}

View File

@@ -3,6 +3,7 @@ using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System.Linq;
using System.Collections.Generic;
using Markdig.Helpers;
namespace Markdig.Tests
{
@@ -12,24 +13,91 @@ namespace Markdig.Tests
[Test]
public void TestSchemas()
{
foreach (var markdown in TestParser.SpecsMarkdown)
foreach (var syntaxTree in TestParser.SpecsSyntaxTrees)
{
AssertSameDescendantsOrder(markdown);
AssertIEnumerablesAreEqual(
Descendants_Legacy(syntaxTree),
syntaxTree.Descendants());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<ParagraphBlock>(),
syntaxTree.Descendants<ParagraphBlock>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<ParagraphBlock>(),
(syntaxTree as ContainerBlock).Descendants<ParagraphBlock>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<LiteralInline>(),
syntaxTree.Descendants<LiteralInline>());
foreach (LiteralInline literalInline in syntaxTree.Descendants<LiteralInline>())
{
Assert.AreSame(ArrayHelper<ListBlock>.Empty, literalInline.Descendants<ListBlock>());
Assert.AreSame(ArrayHelper<ParagraphBlock>.Empty, literalInline.Descendants<ParagraphBlock>());
Assert.AreSame(ArrayHelper<ContainerInline>.Empty, literalInline.Descendants<ContainerInline>());
}
foreach (ContainerInline containerInline in syntaxTree.Descendants<ContainerInline>())
{
AssertIEnumerablesAreEqual(
containerInline.FindDescendants<LiteralInline>(),
containerInline.Descendants<LiteralInline>());
AssertIEnumerablesAreEqual(
containerInline.FindDescendants<LiteralInline>(),
(containerInline as MarkdownObject).Descendants<LiteralInline>());
if (containerInline.FirstChild is null)
{
Assert.AreSame(ArrayHelper<LiteralInline>.Empty, containerInline.Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper<LiteralInline>.Empty, containerInline.FindDescendants<LiteralInline>());
Assert.AreSame(ArrayHelper<LiteralInline>.Empty, (containerInline as MarkdownObject).Descendants<LiteralInline>());
}
Assert.AreSame(ArrayHelper<ListBlock>.Empty, containerInline.Descendants<ListBlock>());
Assert.AreSame(ArrayHelper<ParagraphBlock>.Empty, containerInline.Descendants<ParagraphBlock>());
}
foreach (ParagraphBlock paragraphBlock in syntaxTree.Descendants<ParagraphBlock>())
{
AssertIEnumerablesAreEqual(
(paragraphBlock as MarkdownObject).Descendants<LiteralInline>(),
paragraphBlock.Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper<ParagraphBlock>.Empty, paragraphBlock.Descendants<ParagraphBlock>());
}
foreach (ContainerBlock containerBlock in syntaxTree.Descendants<ContainerBlock>())
{
AssertIEnumerablesAreEqual(
containerBlock.Descendants<LiteralInline>(),
(containerBlock as MarkdownObject).Descendants<LiteralInline>());
AssertIEnumerablesAreEqual(
containerBlock.Descendants<ParagraphBlock>(),
(containerBlock as MarkdownObject).Descendants<ParagraphBlock>());
if (containerBlock.Count == 0)
{
Assert.AreSame(ArrayHelper<LiteralInline>.Empty, containerBlock.Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper<LiteralInline>.Empty, (containerBlock as Block).Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper<LiteralInline>.Empty, (containerBlock as MarkdownObject).Descendants<LiteralInline>());
}
}
}
}
private void AssertSameDescendantsOrder(string markdown)
private static void AssertIEnumerablesAreEqual<T>(IEnumerable<T> first, IEnumerable<T> second)
{
var syntaxTree = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
var firstList = new List<T>(first);
var secondList = new List<T>(second);
var descendants_legacy = Descendants_Legacy(syntaxTree).ToList();
var descendants_new = syntaxTree.Descendants().ToList();
Assert.AreEqual(firstList.Count, secondList.Count);
Assert.AreEqual(descendants_legacy.Count, descendants_new.Count);
for (int i = 0; i < descendants_legacy.Count; i++)
for (int i = 0; i < firstList.Count; i++)
{
Assert.AreSame(descendants_legacy[i], descendants_new[i]);
Assert.AreSame(firstList[i], secondList[i]);
}
}

View File

@@ -0,0 +1,87 @@
using Markdig.Extensions.MediaLinks;
using NUnit.Framework;
using System;
using System.Text.RegularExpressions;
namespace Markdig.Tests
{
[TestFixture]
public class TestMediaLinks
{
private MarkdownPipeline GetPipeline(MediaOptions options = null)
{
return new MarkdownPipelineBuilder()
.UseMediaLinks(options)
.Build();
}
[Test]
[TestCase("![static mp4](https://sample.com/video.mp4)", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"https://sample.com/video.mp4\"></source></video></p>\n")]
[TestCase("![static mp4](//sample.com/video.mp4)", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase("![yandex.ru](https://music.yandex.ru/album/411845/track/4402274)", "<p><iframe src=\"https://music.yandex.ru/iframe/#track/4402274/411845/\" width=\"500\" height=\"281\" class=\"yandex\" frameborder=\"0\"></iframe></p>\n")]
[TestCase("![vimeo](https://vimeo.com/8607834)", "<p><iframe src=\"https://player.vimeo.com/video/8607834\" width=\"500\" height=\"281\" class=\"vimeo\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase("![ok.ru](https://ok.ru/video/26870090463)", "<p><iframe src=\"https://ok.ru/videoembed/26870090463\" width=\"500\" height=\"281\" class=\"odnoklassniki\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase("![ok.ru](//ok.ru/video/26870090463)", "<p><iframe src=\"https://ok.ru/videoembed/26870090463\" width=\"500\" height=\"281\" class=\"odnoklassniki\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
public void TestBuiltInHosts(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
}
private class TestHostProvider : IHostProvider
{
public string Class { get; } = "regex";
public bool AllowFullScreen { get; }
public bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl)
{
iframeUrl = null;
var uri = isSchemaRelative ? "//" + mediaUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme, UriFormat.UriEscaped) : mediaUri.ToString();
if (!matcher.IsMatch(uri))
return false;
iframeUrl = matcher.Replace(uri, replacement);
return true;
}
private Regex matcher;
private string replacement;
public TestHostProvider(string provider, string replace)
{
matcher = new Regex(provider);
replacement = replace;
}
}
[Test]
[TestCase("![p1](https://sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4\" width=\"500\" height=\"281\" class=\"regex\" frameborder=\"0\"></iframe></p>\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")]
[TestCase("![p1](//sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4\" width=\"500\" height=\"281\" class=\"regex\" frameborder=\"0\"></iframe></p>\n", @"^//sample.com/(.+)$", @"https://example.com/$1")]
[TestCase("![p1](https://sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4?token=aaabbb\" width=\"500\" height=\"281\" class=\"regex\" frameborder=\"0\"></iframe></p>\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")]
public void TestCustomHostProvider(string markdown, string expected, string provider, string replace)
{
string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions
{
Hosts =
{
new TestHostProvider(provider, replace),
}
}));
Assert.AreEqual(html, expected);
}
[Test]
[TestCase("![static mp4](//sample.com/video.mp4)", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n", "")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" class=\"youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n", "")]
[TestCase("![static mp4](//sample.com/video.mp4)", "<p><video width=\"500\" height=\"281\" controls=\"\" class=\"k\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n", "k")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" width=\"500\" height=\"281\" class=\"k youtube\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n", "k")]
public void TestCustomClass(string markdown, string expected, string klass)
{
string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions
{
Class = klass,
}));
Assert.AreEqual(html, expected);
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Markdig.Extensions.JiraLinks;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
@@ -33,9 +34,9 @@ namespace Markdig.Tests
// If file creation times aren't preserved by git, add some leeway
// If specs have come from git, assume that they were regenerated since CI would fail otherwise
testTime = testTime.AddSeconds(2);
testTime = testTime.AddMinutes(3);
// This might not catch a changed spec every time, but should most of the time. Otherwise CI will catch it
// This might not catch a changed spec every time, but should at least sometimes. Otherwise CI will catch it
// This could also trigger, if a user has modified the spec file but reverted the change - can't think of a good workaround
Assert.Less(specTime, testTime,
@@ -151,6 +152,11 @@ namespace Markdig.Tests
/// Contains the markdown source for specification files (order is the same as in <see cref="SpecsFilePaths"/>)
/// </summary>
public static readonly string[] SpecsMarkdown;
/// <summary>
/// Contains the markdown syntax tree for specification files (order is the same as in <see cref="SpecsFilePaths"/>)
/// </summary>
public static readonly MarkdownDocument[] SpecsSyntaxTrees;
static TestParser()
{
SpecsFilePaths = Directory.GetDirectories(TestsDirectory)
@@ -161,10 +167,16 @@ namespace Markdig.Tests
.ToArray();
SpecsMarkdown = new string[SpecsFilePaths.Length];
SpecsSyntaxTrees = new MarkdownDocument[SpecsFilePaths.Length];
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < SpecsFilePaths.Length; i++)
{
SpecsMarkdown[i] = File.ReadAllText(SpecsFilePaths[i]);
string markdown = SpecsMarkdown[i] = File.ReadAllText(SpecsFilePaths[i]);
SpecsSyntaxTrees[i] = Markdown.Parse(markdown, pipeline);
}
}
}

View File

@@ -57,7 +57,7 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
> some other text";
var doc = Markdown.Parse(text);
Assert.True(doc.Descendants().OfType<LiteralInline>().All(x => !x.Content.IsEmpty),
Assert.True(doc.Descendants<LiteralInline>().All(x => !x.Content.IsEmpty),
"There should not have any empty literals");
}

View File

@@ -6,8 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Markdig.Extensions.Footnotes;
using Markdig.Helpers;
using Markdig.Extensions.Footnotes;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -147,7 +146,7 @@ literal ( 0, 6) 6-7
public void TestFootnoteLinkReferenceDefinition()
{
// 01 2 345678
var footnote = Markdown.Parse("0\n\n [^1]:", new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseFootnotes().Build()).Descendants().OfType<FootnoteLinkReferenceDefinition>().FirstOrDefault();
var footnote = Markdown.Parse("0\n\n [^1]:", new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseFootnotes().Build()).Descendants<FootnoteLinkReferenceDefinition>().FirstOrDefault();
Assert.NotNull(footnote);
Assert.AreEqual(2, footnote.Line);
@@ -160,7 +159,7 @@ literal ( 0, 6) 6-7
{
// 0 1
// 0123456789012345
var link = Markdown.Parse("[234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkReferenceDefinition>().FirstOrDefault();
var link = Markdown.Parse("[234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants<LinkReferenceDefinition>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(0, link.Line);
@@ -175,7 +174,7 @@ literal ( 0, 6) 6-7
{
// 0 1
// 01 2 34567890123456789
var link = Markdown.Parse("0\n\n [234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkReferenceDefinition>().FirstOrDefault();
var link = Markdown.Parse("0\n\n [234]: /56 'yo' ", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants<LinkReferenceDefinition>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(2, link.Line);
@@ -213,7 +212,7 @@ literal ( 0, 4) 4-5
{
// 0 1
// 01 2 3456789012345
var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
@@ -226,7 +225,7 @@ literal ( 0, 4) 4-5
{
// 0 1
// 01 2 34567890123456789
var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
@@ -240,7 +239,7 @@ literal ( 0, 4) 4-5
{
// 0 1
// 01 2 3456789012345
var link = Markdown.Parse("0\n\n01![234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
var link = Markdown.Parse("0\n\n01![234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(5, 15), link.Span);
@@ -408,7 +407,7 @@ literal ( 1, 2) 6-6
6. Bar
987123. FooBar";
test = test.Replace("\r\n", "\n");
var list = Markdown.Parse(test, new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<ListBlock>().FirstOrDefault();
var list = Markdown.Parse(test, new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants<ListBlock>().FirstOrDefault();
Assert.NotNull(list);
Assert.AreEqual(1, list.Line);

View File

@@ -1,4 +1,4 @@
using NUnit.Framework;
using NUnit.Framework;
using System.Collections.Generic;
using System.Text;
using Markdig.Helpers;
@@ -81,8 +81,18 @@ namespace Markdig.Tests
public void TestSkipWhitespaces()
{
var text = new StringLineGroup(" ABC").ToCharIterator();
Assert.True(text.TrimStart());
Assert.False(text.TrimStart());
Assert.AreEqual('A', text.CurrentChar);
text = new StringLineGroup(" ").ToCharIterator();
Assert.True(text.TrimStart());
Assert.AreEqual('\0', text.CurrentChar);
var slice = new StringSlice(" ABC");
Assert.False(slice.TrimStart());
slice = new StringSlice(" ");
Assert.True(slice.TrimStart());
}
[Test]

View File

@@ -22,6 +22,7 @@ namespace Markdig.Extensions.AutoIdentifiers
{
private const string AutoIdentifierKey = "AutoIdentifier";
private readonly AutoIdentifierOptions options;
private readonly StripRendererCache rendererCache = new StripRendererCache();
/// <summary>
/// Initializes a new instance of the <see cref="AutoIdentifierExtension"/> class.
@@ -159,17 +160,11 @@ namespace Markdig.Extensions.AutoIdentifiers
}
// Use internally a HtmlRenderer to strip links from a heading
var headingWriter = new StringWriter();
var stripRenderer = new HtmlRenderer(headingWriter)
{
// Set to false both to avoid having any HTML tags in the output
EnableHtmlForInline = false,
EnableHtmlEscape = false
};
var stripRenderer = rendererCache.Get();
stripRenderer.Render(headingBlock.Inline);
var headingText = headingWriter.ToString();
headingWriter.GetStringBuilder().Length = 0;
var headingText = stripRenderer.Writer.ToString();
rendererCache.Release(stripRenderer);
// Urilize the link
headingText = (options & AutoIdentifierOptions.GitHub) != 0
@@ -195,5 +190,25 @@ namespace Markdig.Extensions.AutoIdentifiers
attributes.Id = headingId;
}
private sealed class StripRendererCache : ObjectCache<HtmlRenderer>
{
protected override HtmlRenderer NewInstance()
{
var headingWriter = new StringWriter();
var stripRenderer = new HtmlRenderer(headingWriter)
{
// Set to false both to avoid having any HTML tags in the output
EnableHtmlForInline = false,
EnableHtmlEscape = false
};
return stripRenderer;
}
protected override void Reset(HtmlRenderer instance)
{
instance.Reset();
}
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Markdig.Extensions.AutoLinks
/// <summary>
/// The inline parser used to for autolinks.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
/// <seealso cref="InlineParser" />
public class AutoLinkParser : InlineParser
{
/// <summary>
@@ -30,10 +30,14 @@ namespace Markdig.Extensions.AutoLinks
'f', // for ftp://
'm', // for mailto:
'w', // for www.
};
}
public readonly AutoLinkOptions Options;
};
_listOfCharCache = new ListOfCharCache();
}
public readonly AutoLinkOptions Options;
private readonly ListOfCharCache _listOfCharCache;
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
@@ -44,159 +48,162 @@ namespace Markdig.Extensions.AutoLinks
return false;
}
List<char> pendingEmphasis;
// Check that an autolink is possible in the current context
if (!IsAutoLinkValidInCurrentContext(processor, out pendingEmphasis))
{
return false;
}
var startPosition = slice.Start;
int domainOffset = 0;
var c = slice.CurrentChar;
// Precheck URL
switch (c)
{
case 'h':
if (slice.MatchLowercase("ttp://", 1))
{
domainOffset = 7; // http://
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
{
return false;
}
domainOffset = 4; // www.
break;
}
// Parse URL
string link;
if (!LinkHelper.TryParseUrl(ref slice, out link, true))
{
return false;
}
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
if (pendingEmphasis != null)
{
for (int i = link.Length - 1; i >= 0; i--)
{
if (pendingEmphasis.Contains(link[i]))
{
slice.Start--;
}
else
{
if (i < link.Length - 1)
{
link = link.Substring(0, i + 1);
}
break;
}
}
}
// Post-check URL
switch (c)
{
case 'h':
if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) ||
string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'f':
if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'm':
int atIndex = link.IndexOf('@');
if (atIndex == -1 ||
atIndex == 7) // mailto:@ - no email part
{
return false;
}
domainOffset = atIndex + 1;
break;
}
if (!LinkHelper.IsValidDomain(link, domainOffset))
List<char> pendingEmphasis = _listOfCharCache.Get();
try
{
return false;
// Check that an autolink is possible in the current context
if (!IsAutoLinkValidInCurrentContext(processor, pendingEmphasis))
{
return false;
}
var startPosition = slice.Start;
int domainOffset = 0;
var c = slice.CurrentChar;
// Precheck URL
switch (c)
{
case 'h':
if (slice.MatchLowercase("ttp://", 1))
{
domainOffset = 7; // http://
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
{
return false;
}
domainOffset = 4; // www.
break;
}
// Parse URL
if (!LinkHelper.TryParseUrl(ref slice, out string link, true))
{
return false;
}
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
if (pendingEmphasis.Count > 0)
{
for (int i = link.Length - 1; i >= 0; i--)
{
if (pendingEmphasis.Contains(link[i]))
{
slice.Start--;
}
else
{
if (i < link.Length - 1)
{
link = link.Substring(0, i + 1);
}
break;
}
}
}
// Post-check URL
switch (c)
{
case 'h':
if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) ||
string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'f':
if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'm':
int atIndex = link.IndexOf('@');
if (atIndex == -1 ||
atIndex == 7) // mailto:@ - no email part
{
return false;
}
domainOffset = atIndex + 1;
break;
}
if (!LinkHelper.IsValidDomain(link, domainOffset))
{
return false;
}
var inline = new LinkInline()
{
Span =
{
Start = processor.GetSourcePosition(startPosition, out int line, out int column),
},
Line = line,
Column = column,
Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link,
IsClosed = true,
IsAutoLink = true,
};
var skipFromBeginning = c == 'm' ? 7 : 0; // For mailto: skip "mailto:" for content
inline.Span.End = inline.Span.Start + link.Length - 1;
inline.UrlSpan = inline.Span;
inline.AppendChild(new LiteralInline()
{
Span = inline.Span,
Line = line,
Column = column,
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
IsClosed = true
});
processor.Inline = inline;
if (Options.OpenInNewWindow)
{
inline.GetAttributes().AddPropertyIfNotExist("target", "blank");
}
return true;
}
var inline = new LinkInline()
{
Span =
{
Start = processor.GetSourcePosition(startPosition, out int line, out int column),
},
Line = line,
Column = column,
Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link,
IsClosed = true,
IsAutoLink = true,
};
var skipFromBeginning = c == 'm' ? 7 : 0; // For mailto: skip "mailto:" for content
inline.Span.End = inline.Span.Start + link.Length - 1;
inline.UrlSpan = inline.Span;
inline.AppendChild(new LiteralInline()
{
Span = inline.Span,
Line = line,
Column = column,
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
IsClosed = true
});
processor.Inline = inline;
if (Options.OpenInNewWindow)
finally
{
inline.GetAttributes().AddPropertyIfNotExist("target", "blank");
_listOfCharCache.Release(pendingEmphasis);
}
return true;
}
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, out List<char> pendingEmphasis)
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, List<char> pendingEmphasis)
{
pendingEmphasis = null;
// Case where there is a pending HtmlInline <a>
var currentInline = processor.Inline;
while (currentInline != null)
{
var htmlInline = currentInline as HtmlInline;
if (htmlInline != null)
if (currentInline is HtmlInline htmlInline)
{
// If we have a </a> we don't expect nested <a>
if (htmlInline.Tag.StartsWith("</a", StringComparison.OrdinalIgnoreCase))
@@ -211,7 +218,7 @@ namespace Markdig.Extensions.AutoLinks
}
}
// Check previous sibling and parents in the tree
// Check previous sibling and parents in the tree
currentInline = currentInline.PreviousSibling ?? currentInline.Parent;
}
@@ -221,8 +228,7 @@ namespace Markdig.Extensions.AutoLinks
int countBrackets = 0;
while (currentInline != null)
{
var linkDelimiterInline = currentInline as LinkDelimiterInline;
if (linkDelimiterInline != null && linkDelimiterInline.IsActive)
if (currentInline is LinkDelimiterInline linkDelimiterInline && linkDelimiterInline.IsActive)
{
if (linkDelimiterInline.Type == DelimiterType.Open)
{
@@ -236,14 +242,8 @@ namespace Markdig.Extensions.AutoLinks
else
{
// Record all pending characters for emphasis
var emphasisDelimiter = currentInline as EmphasisDelimiterInline;
if (emphasisDelimiter != null)
if (currentInline is EmphasisDelimiterInline emphasisDelimiter)
{
if (pendingEmphasis == null)
{
// Not optimized for GC, but we don't expect this case much
pendingEmphasis = new List<char>();
}
if (!pendingEmphasis.Contains(emphasisDelimiter.DelimiterChar))
{
pendingEmphasis.Add(emphasisDelimiter.DelimiterChar);
@@ -254,6 +254,14 @@ namespace Markdig.Extensions.AutoLinks
}
return countBrackets <= 0;
}
private sealed class ListOfCharCache : DefaultObjectCache<List<char>>
{
protected override void Reset(List<char> instance)
{
instance.Clear();
}
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
@@ -51,8 +51,7 @@ namespace Markdig.Extensions.Bootstrap
}
else if (node is Inline)
{
var link = node as LinkInline;
if (link != null && link.IsImage)
if (node is LinkInline link && link.IsImage)
{
link.GetAttributes().AddClass("img-fluid");
}

View File

@@ -7,24 +7,24 @@ using Markdig.Renderers;
namespace Markdig.Extensions.Emoji
{
/// <summary>
/// Extension to allow emoji and smiley replacement.
/// Extension to allow emoji shortcodes and smileys replacement.
/// </summary>
/// <seealso cref="Markdig.IMarkdownExtension" />
public class EmojiExtension : IMarkdownExtension
{
public EmojiExtension(bool enableSmiley = true)
{
EnableSmiley = enableSmiley;
public EmojiExtension(EmojiMapping emojiMapping)
{
EmojiMapping = emojiMapping;
}
public bool EnableSmiley { get; set; }
public EmojiMapping EmojiMapping { get; }
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<EmojiParser>())
{
// Insert the parser before any other parsers
pipeline.InlineParsers.Insert(0, new EmojiParser(EnableSmiley));
pipeline.InlineParsers.Insert(0, new EmojiParser(EmojiMapping));
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
@@ -8,7 +8,7 @@ using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.Emoji
{
/// <summary>
/// An emoji inline
/// An emoji inline.
/// </summary>
/// <seealso cref="Markdig.Syntax.Inlines.Inline" />
public class EmojiInline : LiteralInline
@@ -32,7 +32,7 @@ namespace Markdig.Extensions.Emoji
}
/// <summary>
/// Gets or sets the original match string (either an emoji or a text smiley)
/// Gets or sets the original match string (either an emoji shortcode or a text smiley)
/// </summary>
public string Match { get; set; }
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
@@ -16,7 +16,7 @@ namespace Markdig.Extensions.GenericAttributes
/// Extension that allows to attach HTML attributes to the previous <see cref="Inline"/> or current <see cref="Block"/>.
/// This extension should be enabled last after enabling other extensions.
/// </summary>
/// <seealso cref="Markdig.IMarkdownExtension" />
/// <seealso cref="IMarkdownExtension" />
public class GenericAttributesExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
@@ -29,8 +29,7 @@ namespace Markdig.Extensions.GenericAttributes
// Plug into all IAttributesParseable
foreach (var parser in pipeline.BlockParsers)
{
var attributesParseable = parser as IAttributesParseable;
if (attributesParseable != null)
if (parser is IAttributesParseable attributesParseable)
{
attributesParseable.TryParseAttributes = TryProcessAttributesForHeading;
}
@@ -53,8 +52,7 @@ namespace Markdig.Extensions.GenericAttributes
var copy = line;
copy.Start = indexOfAttributes;
var startOfAttributes = copy.Start;
HtmlAttributes attributes;
if (GenericAttributesParser.TryParse(ref copy, out attributes))
if (GenericAttributesParser.TryParse(ref copy, out HtmlAttributes attributes))
{
var htmlAttributes = block.GetAttributes();
attributes.CopyTo(htmlAttributes);

View File

@@ -14,7 +14,7 @@ namespace Markdig.Extensions.GenericAttributes
/// <summary>
/// An inline parser used to parse a HTML attributes that can be attached to the previous <see cref="Inline"/> or current <see cref="Block"/>.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
/// <seealso cref="InlineParser" />
public class GenericAttributesParser : InlineParser
{
/// <summary>
@@ -27,9 +27,8 @@ namespace Markdig.Extensions.GenericAttributes
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
HtmlAttributes attributes;
var startPosition = slice.Start;
if (TryParse(ref slice, out attributes))
if (TryParse(ref slice, out HtmlAttributes attributes))
{
var inline = processor.Inline;
@@ -50,8 +49,10 @@ namespace Markdig.Extensions.GenericAttributes
// If the current block is a Paragraph, but only the HtmlAttributes is used,
// Try to attach the attributes to the following block
var paragraph = objectToAttach as ParagraphBlock;
if (paragraph != null && paragraph.Inline.FirstChild == null && processor.Inline == null && slice.IsEmptyOrWhitespace())
if (objectToAttach is ParagraphBlock paragraph &&
paragraph.Inline.FirstChild == null &&
processor.Inline == null &&
slice.IsEmptyOrWhitespace())
{
var parent = paragraph.Parent;
var indexOfParagraph = parent.IndexOf(paragraph);
@@ -67,9 +68,7 @@ namespace Markdig.Extensions.GenericAttributes
attributes.CopyTo(currentHtmlAttributes, true, false);
// Update the position of the attributes
int line;
int column;
currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out line, out column);
currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out int line, out int column);
currentHtmlAttributes.Line = line;
currentHtmlAttributes.Column = column;
currentHtmlAttributes.Span.End = currentHtmlAttributes.Span.Start + slice.Start - startPosition - 1;
@@ -223,6 +222,7 @@ namespace Markdig.Extensions.GenericAttributes
{
// Parse until we match a space or a special html character
startValue = line.Start;
bool valid = false;
while (true)
{
if (c == '\0')
@@ -234,9 +234,10 @@ namespace Markdig.Extensions.GenericAttributes
break;
}
c = line.NextChar();
valid = true;
}
endValue = line.Start - 1;
if (endValue == startValue)
if (!valid)
{
break;
}

View File

@@ -38,7 +38,7 @@ namespace Markdig.Extensions.Globalization
var attributes = node.GetAttributes();
attributes.AddPropertyIfNotExist("dir", "rtl");
if (node is Table table)
if (node is Table)
{
attributes.AddPropertyIfNotExist("align", "right");
}
@@ -71,19 +71,16 @@ namespace Markdig.Extensions.Globalization
}
else if (item is LiteralInline literal)
{
return StartsWithRtlCharacter(literal.ToString());
return StartsWithRtlCharacter(literal.Content);
}
foreach (var descendant in item.Descendants())
foreach (var paragraph in item.Descendants<ParagraphBlock>())
{
if (descendant is ParagraphBlock p)
foreach (var inline in paragraph.Inline)
{
foreach (var i in p.Inline)
if (inline is LiteralInline literal)
{
if (i is LiteralInline l)
{
return StartsWithRtlCharacter(l.ToString());
}
return StartsWithRtlCharacter(literal.Content);
}
}
}
@@ -91,9 +88,9 @@ namespace Markdig.Extensions.Globalization
return false;
}
private bool StartsWithRtlCharacter(string text)
private bool StartsWithRtlCharacter(StringSlice slice)
{
foreach (var c in CharHelper.ToUtf32(text))
foreach (var c in CharHelper.ToUtf32(slice))
{
if (CharHelper.IsRightToLeft(c))
return true;

View File

@@ -0,0 +1,137 @@
// 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 System.Collections.Generic;
using System.Linq;
namespace Markdig.Extensions.MediaLinks
{
public class HostProviderBuilder
{
private sealed class DelegateProvider : IHostProvider
{
public string HostPrefix { get; set; }
public Func<Uri, string> Delegate { get; set; }
public bool AllowFullScreen { get; set; } = true;
public string Class { get; set; }
public bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl)
{
if (!mediaUri.Host.StartsWith(HostPrefix, StringComparison.OrdinalIgnoreCase))
{
iframeUrl = null;
return false;
}
iframeUrl = Delegate(mediaUri);
return !string.IsNullOrEmpty(iframeUrl);
}
}
/// <summary>
/// Create a <see cref="IHostProvider"/> with delegate handler.
/// </summary>
/// <param name="hostPrefix">Prefix of host that can be handled.</param>
/// <param name="handler">Handler that generate iframe url, if uri cannot be handled, it can return <see langword="null"/>.</param>
/// <param name="allowFullScreen">Should the generated iframe has allowfullscreen attribute.</param>
/// <param name="iframeClass">"class" attribute of generated iframe.</param>
/// <returns>A <see cref="IHostProvider"/> with delegate handler.</returns>
public static IHostProvider Create(string hostPrefix, Func<Uri, string> handler, bool allowFullScreen = true, string iframeClass = null)
{
if (string.IsNullOrEmpty(hostPrefix))
throw new ArgumentException("hostPrefix is null or empty.", nameof(hostPrefix));
if (handler == null)
throw new ArgumentNullException(nameof(handler));
return new DelegateProvider { HostPrefix = hostPrefix, Delegate = handler, AllowFullScreen = allowFullScreen, Class = iframeClass };
}
internal static Dictionary<string, IHostProvider> KnownHosts { get; }
= new Dictionary<string, IHostProvider>(StringComparer.OrdinalIgnoreCase)
{
["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"),
["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
["Yandex"] = Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"),
["Odnoklassniki"] = Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"),
};
#region Known providers
private static readonly string[] SplitAnd = { "&" };
private static string[] SplitQuery(Uri uri)
{
var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1);
return query.Split(SplitAnd, StringSplitOptions.RemoveEmptyEntries);
}
private static string YouTube(Uri uri)
{
string uriPath = uri.AbsolutePath;
if (string.Equals(uriPath, "/embed", StringComparison.OrdinalIgnoreCase) || uriPath.StartsWith("/embed/", StringComparison.OrdinalIgnoreCase))
{
return uri.ToString();
}
if (!string.Equals(uriPath, "/watch", StringComparison.OrdinalIgnoreCase) && !uriPath.StartsWith("/watch/", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var queryParams = SplitQuery(uri);
return BuildYouTubeIframeUrl(
queryParams.FirstOrDefault(p => p.StartsWith("v="))?.Substring(2),
queryParams.FirstOrDefault(p => p.StartsWith("t="))?.Substring(2)
);
}
private static string YouTubeShortened(Uri uri)
{
return BuildYouTubeIframeUrl(
uri.AbsolutePath.Substring(1),
SplitQuery(uri).FirstOrDefault(p => p.StartsWith("t="))?.Substring(2)
);
}
private static string BuildYouTubeIframeUrl(string videoId, string startTime)
{
if (string.IsNullOrEmpty(videoId))
{
return null;
}
string url = $"https://www.youtube.com/embed/{videoId}";
return string.IsNullOrEmpty(startTime) ? url : $"{url}?start={startTime}";
}
private static string Vimeo(Uri uri)
{
var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/');
return items.Length > 0 ? $"https://player.vimeo.com/video/{ items[items.Length - 1] }" : null;
}
private static string Odnoklassniki(Uri uri)
{
var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/');
return items.Length > 0 ? $"https://ok.ru/videoembed/{ items[items.Length - 1] }" : null;
}
private static string Yandex(Uri uri)
{
var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/');
var albumKeyword
= items.Skip(0).FirstOrDefault();
var albumId
= items.Skip(1).FirstOrDefault();
var trackKeyword
= items.Skip(2).FirstOrDefault();
var trackId
= items.Skip(3).FirstOrDefault();
if (albumKeyword != "album" || albumId == null || trackKeyword != "track" || trackId == null)
{
return null;
}
return $"https://music.yandex.ru/iframe/#track/{trackId}/{albumId}/";
}
#endregion
}
}

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;
namespace Markdig.Extensions.MediaLinks
{
/// <summary>
/// Provides url for media links.
/// </summary>
public interface IHostProvider
{
/// <summary>
/// "class" attribute of generated iframe.
/// </summary>
string Class { get; }
/// <summary>
/// Generate url for iframe.
/// </summary>
/// <param name="mediaUri">Input media uri.</param>
/// <param name="isSchemaRelative"><see langword="true"/> if <paramref name="mediaUri"/> is a schema relative uri, i.e. uri starts with "//".</param>
/// <param name="iframeUrl">Generated url for iframe.</param>
/// <seealso href="https://tools.ietf.org/html/rfc3986#section-4.2"/>
bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl);
/// <summary>
/// Should the generated iframe has allowfullscreen attribute.
/// </summary>
/// <remarks>
/// Should be false for audio embedding.
/// </remarks>
bool AllowFullScreen { get; }
}
}

View File

@@ -3,7 +3,6 @@
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Markdig.Renderers;
using Markdig.Renderers.Html;
@@ -55,18 +54,28 @@ namespace Markdig.Extensions.MediaLinks
}
Uri uri;
bool isSchemaRelative = false;
// Only process absolute Uri
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri) || !uri.IsAbsoluteUri)
{
return false;
// see https://tools.ietf.org/html/rfc3986#section-4.2
// since relative uri doesn't support many properties, "http" is used as a placeholder here.
if (linkInline.Url.StartsWith("//") && Uri.TryCreate("http:" + linkInline.Url, UriKind.Absolute, out uri))
{
isSchemaRelative = true;
}
else
{
return false;
}
}
if (TryRenderIframeFromKnownProviders(uri, renderer, linkInline))
if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline))
{
return true;
}
if (TryGuessAudioVideoFile(uri, renderer, linkInline))
if (TryGuessAudioVideoFile(uri, isSchemaRelative, renderer, linkInline))
{
return true;
}
@@ -86,7 +95,7 @@ namespace Markdig.Extensions.MediaLinks
return htmlAttributes;
}
private bool TryGuessAudioVideoFile(Uri uri, HtmlRenderer renderer, LinkInline linkInline)
private bool TryGuessAudioVideoFile(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline)
{
var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
// Otherwise try to detect if we have an audio/video from the file extension
@@ -106,6 +115,10 @@ namespace Markdig.Extensions.MediaLinks
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
}
htmlAttributes.AddPropertyIfNotExist("controls", null);
if (!string.IsNullOrEmpty(Options.Class))
htmlAttributes.AddPropertyIfNotExist("class", Options.Class);
renderer.WriteAttributes(htmlAttributes);
renderer.Write($"><source type=\"{mimeType}\" src=\"{linkInline.Url}\"></source></{tagType}>");
@@ -115,38 +128,18 @@ namespace Markdig.Extensions.MediaLinks
return false;
}
#region Known providers
private class KnownProvider
private bool TryRenderIframeFromKnownProviders(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline)
{
public string HostPrefix { get; set; }
public Func<Uri, string> Delegate { get; set; }
public bool AllowFullScreen { get; set; } = true; //Should be false for audio embedding
}
private static readonly List<KnownProvider> KnownHosts = new List<KnownProvider>()
{
new KnownProvider {HostPrefix = "www.youtube.com", Delegate = YouTube},
new KnownProvider {HostPrefix = "youtu.be", Delegate = YouTubeShortened},
new KnownProvider {HostPrefix = "vimeo.com", Delegate = Vimeo},
new KnownProvider {HostPrefix = "music.yandex.ru", Delegate = Yandex, AllowFullScreen = false},
new KnownProvider {HostPrefix = "ok.ru", Delegate = Odnoklassniki},
};
private bool TryRenderIframeFromKnownProviders(Uri uri, HtmlRenderer renderer, LinkInline linkInline)
{
var foundProvider =
KnownHosts
.Where(pair => uri.Host.StartsWith(pair.HostPrefix, StringComparison.OrdinalIgnoreCase)) // when host is match
.Select(provider =>
new
{
provider.AllowFullScreen,
Result = provider.Delegate(uri) // try to call delegate to get iframeUrl
}
)
.FirstOrDefault(provider => provider.Result != null); // use first success
IHostProvider foundProvider = null;
string iframeUrl = null;
foreach (var provider in Options.Hosts)
{
if (!provider.TryHandle(uri, isSchemaRelative, out iframeUrl))
continue;
foundProvider = provider;
break;
}
if (foundProvider == null)
{
@@ -155,17 +148,20 @@ namespace Markdig.Extensions.MediaLinks
var htmlAttributes = GetHtmlAttributes(linkInline);
renderer.Write("<iframe src=\"");
renderer.WriteEscapeUrl(foundProvider.Result);
renderer.WriteEscapeUrl(iframeUrl);
renderer.Write("\"");
if(!string.IsNullOrEmpty(Options.Width))
if (!string.IsNullOrEmpty(Options.Width))
htmlAttributes.AddPropertyIfNotExist("width", Options.Width);
if (!string.IsNullOrEmpty(Options.Height))
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
if (!string.IsNullOrEmpty(Options.Class))
htmlAttributes.AddPropertyIfNotExist("class", Options.Class);
if (!string.IsNullOrEmpty(Options.Class) || !string.IsNullOrEmpty(foundProvider.Class))
htmlAttributes.AddPropertyIfNotExist("class",
(!string.IsNullOrEmpty(Options.Class) && !string.IsNullOrEmpty(foundProvider.Class))
? Options.Class + " " + foundProvider.Class
: Options.Class + foundProvider.Class);
htmlAttributes.AddPropertyIfNotExist("frameborder", "0");
if (foundProvider.AllowFullScreen)
@@ -177,81 +173,5 @@ namespace Markdig.Extensions.MediaLinks
return true;
}
private static readonly string[] SplitAnd = {"&"};
private static string[] SplitQuery(Uri uri)
{
var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1);
return query.Split(SplitAnd, StringSplitOptions.RemoveEmptyEntries);
}
private static string YouTube(Uri uri)
{
string uriPath = uri.AbsolutePath;
if (string.Equals(uriPath, "/embed", StringComparison.OrdinalIgnoreCase) || uriPath.StartsWith("/embed/", StringComparison.OrdinalIgnoreCase))
{
return uri.ToString();
}
if (!string.Equals(uriPath, "/watch", StringComparison.OrdinalIgnoreCase) && !uriPath.StartsWith("/watch/", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var queryParams = SplitQuery(uri);
return BuildYouTubeIframeUrl(
queryParams.FirstOrDefault(p => p.StartsWith("v="))?.Substring(2),
queryParams.FirstOrDefault(p => p.StartsWith("t="))?.Substring(2)
);
}
private static string YouTubeShortened(Uri uri)
{
return BuildYouTubeIframeUrl(
uri.AbsolutePath.Substring(1),
SplitQuery(uri).FirstOrDefault(p => p.StartsWith("t="))?.Substring(2)
);
}
private static string BuildYouTubeIframeUrl(string videoId, string startTime)
{
if (string.IsNullOrEmpty(videoId))
{
return null;
}
string url = $"https://www.youtube.com/embed/{videoId}";
return string.IsNullOrEmpty(startTime) ? url : $"{url}?start={startTime}";
}
private static string Vimeo(Uri uri)
{
var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/');
return items.Length > 0 ? $"https://player.vimeo.com/video/{items[items.Length - 1]}" : null;
}
private static string Odnoklassniki(Uri uri)
{
var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/');
return items.Length > 0 ? $"https://ok.ru/videoembed/{items[items.Length - 1]}" : null;
}
private static string Yandex(Uri uri)
{
var items = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped).Split('/');
var albumKeyword
= items.Skip(0).FirstOrDefault();
var albumId
= items.Skip(1).FirstOrDefault();
var trackKeyword
= items.Skip(2).FirstOrDefault();
var trackId
= items.Skip(3).FirstOrDefault();
if (albumKeyword != "album" || albumId == null || trackKeyword != "track" || trackId == null)
{
return null;
}
return $"https://music.yandex.ru/iframe/#track/{trackId}/{albumId}/";
}
#endregion
}
}

View File

@@ -76,12 +76,14 @@ namespace Markdig.Extensions.MediaLinks
{".ecelp7470", "audio/vnd.nuera.ecelp7470"},
{".ecelp9600", "audio/vnd.nuera.ecelp9600"},
{".oga", "audio/ogg"},
{".ogg", "audio/ogg"},
{".weba", "audio/webm"},
{".ram", "audio/x-pn-realaudio"},
{".rmp", "audio/x-pn-realaudio-plugin"},
{".au", "audio/basic"},
{".wav", "audio/x-wav"},
};
Hosts = new List<IHostProvider>(HostProviderBuilder.KnownHosts.Values);
}
public string Width { get; set; }
@@ -91,5 +93,7 @@ namespace Markdig.Extensions.MediaLinks
public string Class { get; set; }
public Dictionary<string, string> ExtensionToMimeType { get; }
public List<IHostProvider> Hosts { get; }
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
@@ -10,7 +10,7 @@ namespace Markdig.Extensions.SmartyPants
/// <summary>
/// A HTML renderer for a <see cref="SmartyPant"/>.
/// </summary>
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{SmartyPant}" />
/// <seealso cref="HtmlObjectRenderer{SmartyPant}" />
public class HtmlSmartyPantRenderer : HtmlObjectRenderer<SmartyPant>
{
private static readonly SmartyPantOptions DefaultOptions = new SmartyPantOptions();
@@ -21,17 +21,15 @@ namespace Markdig.Extensions.SmartyPants
/// Initializes a new instance of the <see cref="HtmlSmartyPantRenderer"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public HtmlSmartyPantRenderer(SmartyPantOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
this.options = options;
this.options = options ?? throw new ArgumentNullException(nameof(options));
}
protected override void Write(HtmlRenderer renderer, SmartyPant obj)
{
string text;
if (!options.Mapping.TryGetValue(obj.Type, out text))
if (!options.Mapping.TryGetValue(obj.Type, out string text))
{
DefaultOptions.Mapping.TryGetValue(obj.Type, out text);
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics;
@@ -45,5 +45,15 @@ namespace Markdig.Extensions.SmartyPants
}
return OpeningCharacter != 0 ? OpeningCharacter.ToString() : string.Empty;
}
public LiteralInline AsLiteralInline()
{
return new LiteralInline(ToString())
{
Span = Span,
Line = Line,
Column = Column,
};
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
@@ -37,8 +37,7 @@ namespace Markdig.Extensions.SmartyPants
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
if (renderer is HtmlRenderer htmlRenderer)
{
if (!htmlRenderer.ObjectRenderers.Contains<HtmlSmartyPantRenderer>())
{

View File

@@ -49,7 +49,7 @@ namespace Markdig.Extensions.SmartyPants
{
case '\'':
type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines
if (slice.PeekChar(1) == '\'')
if (slice.PeekChar() == '\'')
{
slice.NextChar();
type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines
@@ -95,9 +95,7 @@ namespace Markdig.Extensions.SmartyPants
// Skip char
c = slice.NextChar();
bool canOpen;
bool canClose;
CharHelper.CheckOpenCloseDelimiter(pc, c, false, out canOpen, out canClose);
CharHelper.CheckOpenCloseDelimiter(pc, c, false, out bool canOpen, out bool canClose);
bool postProcess = false;
@@ -156,11 +154,9 @@ namespace Markdig.Extensions.SmartyPants
}
// Create the SmartyPant inline
int line;
int column;
var pant = new SmartyPant()
{
Span = {Start = processor.GetSourcePosition(startingPosition, out line, out column)},
Span = { Start = processor.GetSourcePosition(startingPosition, out int line, out int column) },
Line = line,
Column = column,
OpeningCharacter = openingChar,
@@ -195,96 +191,90 @@ namespace Markdig.Extensions.SmartyPants
return quotePants;
}
private readonly struct Opener
{
public readonly int Type;
public readonly int Index;
public Opener(int type, int index)
{
Type = type;
Index = index;
}
}
private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline inline)
{
processor.Block.ProcessInlinesEnd -= BlockOnProcessInlinesEnd;
var pants = (ListSmartyPants) 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};
Stack<Opener> openers = new Stack<Opener>(4);
for (int i = 0; i < pants.Count; i++)
{
var quote = pants[i];
var quoteType = quote.Type;
int currentTypeIndex = -1;
SmartyPantType expectedLeftQuote = 0;
SmartyPantType expectedRightQuote = 0;
int type;
bool isLeft;
if (quote.Type == SmartyPantType.LeftQuote || quote.Type == SmartyPantType.RightQuote)
if (quoteType == SmartyPantType.LeftQuote || quoteType == SmartyPantType.RightQuote)
{
currentTypeIndex = 0;
expectedLeftQuote = SmartyPantType.LeftQuote;
expectedRightQuote = SmartyPantType.RightQuote;
type = 0;
isLeft = quoteType == SmartyPantType.LeftQuote;
}
else if (quote.Type == SmartyPantType.LeftDoubleQuote || quote.Type == SmartyPantType.RightDoubleQuote)
else if (quoteType == SmartyPantType.LeftDoubleQuote || quoteType == SmartyPantType.RightDoubleQuote)
{
currentTypeIndex = 1;
expectedLeftQuote = SmartyPantType.LeftDoubleQuote;
expectedRightQuote = SmartyPantType.RightDoubleQuote;
type = 1;
isLeft = quoteType == SmartyPantType.LeftDoubleQuote;
}
else if (quote.Type == SmartyPantType.LeftAngleQuote || quote.Type == SmartyPantType.RightAngleQuote)
else if (quoteType == SmartyPantType.LeftAngleQuote || quoteType == 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;
}
type = 2;
isLeft = quoteType == SmartyPantType.LeftAngleQuote;
}
else
{
if (quote.Type == expectedRightQuote)
{
// Replace all intermediate unmatched left or right SmartyPants to their 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.ToString())
{
Span = toReplace.Span,
Line = toReplace.Line,
Column = toReplace.Column,
});
i--;
}
quote.ReplaceBy(quote.AsLiteralInline());
continue;
}
// If we matched, we remove left/right quotes from the list
pants.RemoveAt(previousIndex);
previousIndices[currentTypeIndex] = -1;
}
else
if (isLeft)
{
openers.Push(new Opener(type, i));
}
else
{
bool found = false;
while (openers.Count > 0)
{
previousIndices[currentTypeIndex] = i;
Opener opener = openers.Pop();
var previousQuote = pants[opener.Index];
if (opener.Type == type)
{
found = true;
break;
}
else
{
previousQuote.ReplaceBy(previousQuote.AsLiteralInline());
}
}
if (!found)
{
quote.ReplaceBy(quote.AsLiteralInline());
}
}
}
// If we have any quotes lefts, replace them by there literal equivalent
foreach (var quote in pants)
foreach (var opener in openers)
{
quote.ReplaceBy(new LiteralInline(quote.ToString())
{
Span = quote.Span,
Line = quote.Line,
Column = quote.Column,
});
var quote = pants[opener.Index];
quote.ReplaceBy(quote.AsLiteralInline());
}
pants.Clear();
@@ -294,8 +284,7 @@ namespace Markdig.Extensions.SmartyPants
bool isFinalProcessing)
{
// Don't try to process anything if there are no dash
var quotePants = state.ParserStates[Index] as ListSmartyPants;
if (quotePants == null || !quotePants.HasDash)
if (!(state.ParserStates[Index] is ListSmartyPants quotePants) || !quotePants.HasDash)
{
return true;
}
@@ -309,10 +298,8 @@ namespace Markdig.Extensions.SmartyPants
{
var next = child.NextSibling;
if (child is LiteralInline)
if (child is LiteralInline literal)
{
var literal = (LiteralInline) child;
var startIndex = 0;
var indexOfDash = literal.Content.IndexOf("--", startIndex);
@@ -372,7 +359,7 @@ namespace Markdig.Extensions.SmartyPants
}
private class ListSmartyPants : List<SmartyPant>
private sealed class ListSmartyPants : List<SmartyPant>
{
public bool HasDash { get; set; }
}

View File

@@ -1,4 +1,4 @@
// 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.
@@ -138,7 +138,7 @@ namespace Markdig.Extensions.Tables
private static void SetRowSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
{
var lineStart = line.Start;
isHeaderRow = line.PeekChar(1) == '=' || line.PeekChar(2) == '=';
isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '=';
hasRowSpan = false;
foreach (var columnSlice in columns)
{

View File

@@ -10,7 +10,7 @@ namespace Markdig.Extensions.Tables
/// <summary>
/// Internal state used by the <see cref="GridTableParser"/>
/// </summary>
internal class GridTableState
internal sealed class GridTableState
{
public int Start { get; set; }

View File

@@ -36,7 +36,8 @@ using System;
using System.Globalization;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Diagnostics;
namespace Markdig.Helpers
{
/// <summary>
@@ -130,9 +131,9 @@ namespace Markdig.Helpers
int result = 0;
for (int i = 0; i < text.Length; i++)
{
var character = Char.ToUpperInvariant(text[i]);
var character = char.ToUpperInvariant(text[i]);
var candidate = romanMap[character];
if (i + 1 < text.Length && candidate < romanMap[Char.ToUpperInvariant(text[i + 1])])
if (i + 1 < text.Length && candidate < romanMap[char.ToUpperInvariant(text[i + 1])])
{
result -= candidate;
}
@@ -147,7 +148,9 @@ namespace Markdig.Helpers
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static int AddTab(int column)
{
return ((column + TabSize) / TabSize) * TabSize;
// return ((column + TabSize) / TabSize) * TabSize;
Debug.Assert(TabSize == 4, "Change the AddTab implementation if TabSize is no longer a power of 2");
return TabSize + (column & ~(TabSize - 1));
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
@@ -174,13 +177,13 @@ namespace Markdig.Helpers
{
// 2.1 Characters and lines
// A whitespace character is a space(U + 0020), tab(U + 0009), newline(U + 000A), line tabulation (U + 000B), form feed (U + 000C), or carriage return (U + 000D).
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
return c <= ' ' && (c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r');
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsControl(this char c)
{
return c < ' ' || Char.IsControl(c);
return c < ' ' || char.IsControl(c);
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
@@ -193,7 +196,7 @@ namespace Markdig.Helpers
//[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsWhiteSpaceOrZero(this char c)
{
return IsWhitespace(c) || IsZero(c);
return IsZero(c) || IsWhitespace(c);
}
// Note that we are not considering the character & as a punctuation in HTML
@@ -294,14 +297,14 @@ namespace Markdig.Helpers
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsAlphaUpper(this char c)
{
return c >= 'A' && c <= 'Z';
{
return (uint)(c - 'A') <= ('Z' - 'A');
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsAlpha(this char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
{
return (uint)((c - 'A') & ~0x20) <= ('Z' - 'A');
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
@@ -313,7 +316,7 @@ namespace Markdig.Helpers
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsDigit(this char c)
{
return c >= '0' && c <= '9';
return (uint)(c - '0') <= ('9' - '0');
}
public static bool IsAsciiPunctuation(this char c)
@@ -367,16 +370,20 @@ namespace Markdig.Helpers
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsHighSurrogate(char c)
{
return ((c >= HighSurrogateStart) && (c <= HighSurrogateEnd));
{
return IsInInclusiveRange(c, HighSurrogateStart, HighSurrogateEnd);
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static bool IsLowSurrogate(char c)
{
return ((c >= LowSurrogateStart) && (c <= LowSurrogateEnd));
return IsInInclusiveRange(c, LowSurrogateStart, LowSurrogateEnd);
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
private static bool IsInInclusiveRange(char c, char min, char max)
=> (uint) (c - min) <= (uint) (max - min);
public static int ConvertToUtf32(char highSurrogate, char lowSurrogate)
{
if (!IsHighSurrogate(highSurrogate))
@@ -390,13 +397,14 @@ namespace Markdig.Helpers
return (((highSurrogate - HighSurrogateStart) * 0x400) + (lowSurrogate - LowSurrogateStart) + UnicodePlane01Start);
}
public static IEnumerable<int> ToUtf32(string text)
public static IEnumerable<int> ToUtf32(StringSlice text)
{
for (int i = 0; i < text.Length; i++)
for (int i = text.Start; i <= text.End; i++)
{
if (IsHighSurrogate(text[i]) && i < text.Length - 1 && IsLowSurrogate(text[i + 1]))
{
yield return ConvertToUtf32(text[i], text[i + 1]);
if (IsHighSurrogate(text[i]) && i < text.End && IsLowSurrogate(text[i + 1]))
{
Debug.Assert(char.IsSurrogatePair(text[i], text[i + 1]));
yield return char.ConvertToUtf32(text[i], text[i + 1]);
}
else
{

View File

@@ -25,7 +25,7 @@ namespace Markdig.Helpers
/// <para>Something between a Trie and a full Radix tree, but stored linearly in memory</para>
/// </summary>
/// <typeparam name="TValue">The value associated with the key</typeparam>
internal class CompactPrefixTree<TValue>
internal sealed class CompactPrefixTree<TValue>
//#if !LEGACY
// : IReadOnlyDictionary<string, TValue>, IReadOnlyList<KeyValuePair<string, TValue>>
//#endif

View File

@@ -0,0 +1,86 @@
using System.Threading;
namespace Markdig.Helpers
{
internal sealed class CustomArrayPool<T>
{
private sealed class Bucket
{
private readonly T[][] _buffers;
private int _index;
private int _lock;
public Bucket(int numberOfBuffers)
{
_buffers = new T[numberOfBuffers][];
}
public T[] Rent()
{
T[][] buffers = _buffers;
T[] buffer = null;
if (Interlocked.CompareExchange(ref _lock, 1, 0) == 0)
{
int index = _index;
if ((uint)index < (uint)buffers.Length)
{
buffer = buffers[index];
buffers[index] = null;
_index = index + 1;
}
Interlocked.Decrement(ref _lock);
}
return buffer;
}
public void Return(T[] array)
{
var buffers = _buffers;
if (Interlocked.CompareExchange(ref _lock, 1, 0) == 0)
{
int index = _index - 1;
if ((uint)index < (uint)buffers.Length)
{
buffers[index] = array;
_index = index;
}
Interlocked.Decrement(ref _lock);
}
}
}
private readonly Bucket _bucket4, _bucket8, _bucket16, _bucket32;
public CustomArrayPool(int size4, int size8, int size16, int size32)
{
_bucket4 = new Bucket(size4);
_bucket8 = new Bucket(size8);
_bucket16 = new Bucket(size16);
_bucket32 = new Bucket(size32);
}
private Bucket SelectBucket(int length)
{
switch (length)
{
case 4: return _bucket4;
case 8: return _bucket8;
case 16: return _bucket16;
case 32: return _bucket32;
default: return null;
}
}
public T[] Rent(int length)
{
return SelectBucket(length)?.Rent() ?? new T[length];
}
public void Return(T[] array)
{
SelectBucket(array.Length)?.Return(array);
}
}
}

View File

@@ -373,7 +373,7 @@ namespace Markdig.Helpers
}
builder.Append('-');
builder.Append('-');
if (text.PeekChar(1) == '>')
if (text.PeekChar() == '>')
{
return false;
}

View File

@@ -597,20 +597,23 @@ namespace Markdig.Helpers
}
}
if (hasEscape && !c.IsAsciiPunctuation())
if (!isAutoLink)
{
buffer.Append('\\');
}
if (hasEscape && !c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
// If we have an escape
if (c == '\\')
{
hasEscape = true;
c = text.NextChar();
continue;
}
// If we have an escape
if (c == '\\')
{
hasEscape = true;
c = text.NextChar();
continue;
}
hasEscape = false;
hasEscape = false;
}
if (IsEndOfUri(c, isAutoLink))
{
@@ -622,10 +625,7 @@ namespace Markdig.Helpers
{
if (c == '&')
{
int entityNameStart;
int entityNameLength;
int entityValue;
if (HtmlHelper.ScanEntity(text, out entityValue, out entityNameStart, out entityNameLength) > 0)
if (HtmlHelper.ScanEntity(text, out _, out _, out _) > 0)
{
isValid = true;
break;

View File

@@ -6,7 +6,7 @@ using System.Text;
namespace Markdig.Helpers
{
/// <summary>
/// Extensions for StringBuilder with <see cref="StringSlice"/>
/// Extensions for StringBuilder
/// </summary>
public static class StringBuilderExtensions
{
@@ -19,5 +19,12 @@ namespace Markdig.Helpers
{
return builder.Append(slice.Text, slice.Start, slice.Length);
}
internal static string GetStringAndReset(this StringBuilder builder)
{
string text = builder.ToString();
builder.Length = 0;
return text;
}
}
}

View File

@@ -77,7 +77,7 @@ namespace Markdig.Helpers
return line.Slice;
}
public override string ToString()
public readonly override string ToString()
{
return Slice.ToString();
}

View File

@@ -5,26 +5,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Markdig.Extensions.Tables;
namespace Markdig.Helpers
{
/// <summary>
/// A group of <see cref="StringLine"/>.
/// </summary>
/// <seealso cref="System.Collections.IEnumerable" />
/// <seealso cref="IEnumerable" />
public struct StringLineGroup : IEnumerable
{
// Feel free to change these numbers if you see a positive change
private static readonly CustomArrayPool<StringLine> _pool
= new CustomArrayPool<StringLine>(512, 386, 128, 64);
/// <summary>
/// Initializes a new instance of the <see cref="StringLineGroup"/> class.
/// </summary>
/// <param name="capacity"></param>
public StringLineGroup(int capacity)
public StringLineGroup(int capacity, bool willRelease = false)
{
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
Lines = new StringLine[capacity];
Lines = _pool.Rent(willRelease ? Math.Max(8, capacity) : capacity);
Count = 0;
}
@@ -100,7 +102,7 @@ namespace Markdig.Helpers
Lines[Count++] = new StringLine(ref slice);
}
public override string ToString()
public readonly override string ToString()
{
return ToSlice().ToString();
}
@@ -110,7 +112,7 @@ namespace Markdig.Helpers
/// </summary>
/// <param name="lineOffsets">The position of the `\n` line offsets from the beginning of the returned slice.</param>
/// <returns>A single slice concatenating the lines of this instance</returns>
public StringSlice ToSlice(List<LineOffset> lineOffsets = null)
public readonly StringSlice ToSlice(List<LineOffset> lineOffsets = null)
{
// Optimization case when no lines
if (Count == 0)
@@ -128,6 +130,11 @@ namespace Markdig.Helpers
return Lines[0];
}
if (lineOffsets != null && lineOffsets.Capacity < lineOffsets.Count + Count)
{
lineOffsets.Capacity = Math.Max(lineOffsets.Count + Count, lineOffsets.Capacity * 2);
}
// Else use a builder
var builder = StringBuilderCache.Local();
int previousStartOfLine = 0;
@@ -151,16 +158,14 @@ namespace Markdig.Helpers
{
lineOffsets.Add(new LineOffset(Lines[Count - 1].Position, Lines[Count - 1].Column, Lines[Count - 1].Slice.Start - Lines[Count - 1].Position, previousStartOfLine, builder.Length));
}
var str = builder.ToString();
builder.Length = 0;
return new StringSlice(str);
return new StringSlice(builder.GetStringAndReset());
}
/// <summary>
/// Converts this instance into a <see cref="ICharIterator"/>.
/// </summary>
/// <returns></returns>
public Iterator ToCharIterator()
public readonly Iterator ToCharIterator()
{
return new Iterator(this);
}
@@ -183,14 +188,24 @@ namespace Markdig.Helpers
private void IncreaseCapacity()
{
var newItems = new StringLine[Lines.Length * 2];
var newItems = _pool.Rent(Lines.Length * 2);
if (Count > 0)
{
Array.Copy(Lines, 0, newItems, 0, Count);
Array.Clear(Lines, 0, Count);
}
_pool.Return(Lines);
Lines = newItems;
}
internal void Release()
{
Array.Clear(Lines, 0, Count);
_pool.Return(Lines);
Lines = null;
Count = -1;
}
/// <summary>
/// The iterator used to iterate other the lines.
/// </summary>
@@ -207,7 +222,7 @@ namespace Markdig.Helpers
_offset = -1;
SliceIndex = 0;
CurrentChar = '\0';
End = -2;
End = -2;
for (int i = 0; i < lines.Count; i++)
{
End += lines.Lines[i].Slice.Length + 1; // Add chars
@@ -221,7 +236,7 @@ namespace Markdig.Helpers
public int End { get; private set; }
public bool IsEmpty => Start > End;
public readonly bool IsEmpty => Start > End;
public int SliceIndex { get; private set; }
@@ -277,7 +292,7 @@ namespace Markdig.Helpers
return CurrentChar;
}
public char PeekChar(int offset = 1)
public readonly char PeekChar(int offset = 1)
{
if (offset < 0) throw new ArgumentOutOfRangeException("Negative offset are not supported for StringLineGroup", nameof(offset));
@@ -298,17 +313,15 @@ namespace Markdig.Helpers
public bool TrimStart()
{
var c = CurrentChar;
bool hasSpaces = false;
while (c.IsWhitespace())
{
hasSpaces = true;
c = NextChar();
}
return hasSpaces;
return IsEmpty;
}
}
public struct LineOffset
public readonly struct LineOffset
{
public LineOffset(int linePosition, int column, int offset, int start, int end)
{

View File

@@ -38,8 +38,7 @@ namespace Markdig.Helpers
/// <exception cref="System.ArgumentNullException"></exception>
public StringSlice(string text, int start, int end)
{
if (text == null) throw new ArgumentNullException(nameof(text));
Text = text;
Text = text ?? throw new ArgumentNullException(nameof(text));
Start = start;
End = end;
}
@@ -62,24 +61,40 @@ namespace Markdig.Helpers
/// <summary>
/// Gets the length.
/// </summary>
public int Length => End - Start + 1;
public readonly int Length => End - Start + 1;
/// <summary>
/// Gets the current character.
/// </summary>
public char CurrentChar => Start <= End ? this[Start] : '\0';
public readonly char CurrentChar
{
get
{
int start = Start;
return start <= End ? Text[start] : '\0';
}
}
/// <summary>
/// Gets a value indicating whether this instance is empty.
/// </summary>
public bool IsEmpty => Start > End;
public readonly bool IsEmpty
{
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
get => Start > End;
}
/// <summary>
/// Gets the <see cref="System.Char"/> at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>A character in the slice at the specified index (not from <see cref="Start"/> but from the begining of the slice)</returns>
public char this[int index] => Text[index];
public readonly char this[int index]
{
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
get => Text[index];
}
/// <summary>
/// Goes to the next character, incrementing the <see cref="Start" /> position.
@@ -90,13 +105,27 @@ namespace Markdig.Helpers
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public char NextChar()
{
Start++;
if (Start > End)
int start = Start;
if (start >= End)
{
Start = End + 1;
return '\0';
}
return Text[Start];
start++;
Start = start;
return Text[start];
}
/// <summary>
/// Peeks a character at the offset of 1 from the current <see cref="Start"/> position
/// inside the range <see cref="Start"/> and <see cref="End"/>, returns `\0` if outside this range.
/// </summary>
/// <returns>The character at offset, returns `\0` if none.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public readonly char PeekChar()
{
int index = Start + 1;
return index <= End ? Text[index] : '\0';
}
/// <summary>
@@ -106,10 +135,10 @@ namespace Markdig.Helpers
/// <param name="offset">The offset.</param>
/// <returns>The character at offset, returns `\0` if none.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public char PeekChar(int offset = 1)
public readonly char PeekChar(int offset)
{
var index = Start + offset;
return index >= Start && index <= End ? Text[index] : (char) 0;
return index >= Start && index <= End ? Text[index] : '\0';
}
/// <summary>
@@ -117,9 +146,10 @@ namespace Markdig.Helpers
/// </summary>
/// <returns>The character at offset, returns `\0` if none.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public char PeekCharAbsolute(int index)
public readonly char PeekCharAbsolute(int index)
{
return index >= 0 && index < Text.Length ? Text[index] : (char)0;
string text = Text;
return (uint)index < (uint)text.Length ? text[index] : '\0';
}
/// <summary>
@@ -129,10 +159,11 @@ namespace Markdig.Helpers
/// <param name="offset">The offset.</param>
/// <returns>The character at offset, returns `\0` if none.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public char PeekCharExtra(int offset)
public readonly char PeekCharExtra(int offset)
{
var index = Start + offset;
return index >= 0 && index < Text.Length ? Text[index] : (char)0;
var text = Text;
return (uint)index < (uint)text.Length ? text[index] : '\0';
}
/// <summary>
@@ -141,7 +172,7 @@ namespace Markdig.Helpers
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text matches; <c>false</c> otherwise</returns>
public bool Match(string text, int offset = 0)
public readonly bool Match(string text, int offset = 0)
{
return Match(text, End, offset);
}
@@ -153,19 +184,22 @@ namespace Markdig.Helpers
/// <param name="end">The end.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text matches; <c>false</c> otherwise</returns>
public bool Match(string text, int end, int offset)
public readonly bool Match(string text, int end, int offset)
{
var index = Start + offset;
int i = 0;
for (; index <= end && i < text.Length; i++, index++)
if (end - index + 1 < text.Length)
return false;
string sliceText = Text;
for (int i = 0; i < text.Length; i++, index++)
{
if (text[i] != Text[index])
if (text[i] != sliceText[index])
{
return false;
}
}
return i == text.Length;
return true;
}
/// <summary>
@@ -196,7 +230,7 @@ namespace Markdig.Helpers
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text matches; <c>false</c> otherwise</returns>
public bool MatchLowercase(string text, int offset = 0)
public readonly bool MatchLowercase(string text, int offset = 0)
{
return MatchLowercase(text, End, offset);
}
@@ -208,19 +242,22 @@ namespace Markdig.Helpers
/// <param name="end">The end.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text matches; <c>false</c> otherwise</returns>
public bool MatchLowercase(string text, int end, int offset)
public readonly bool MatchLowercase(string text, int end, int offset)
{
var index = Start + offset;
int i = 0;
for (; index <= end && i < text.Length; i++, index++)
if (end - index + 1 < text.Length)
return false;
string sliceText = Text;
for (int i = 0; i < text.Length; i++, index++)
{
if (text[i] != char.ToLowerInvariant(Text[index]))
if (text[i] != char.ToLowerInvariant(sliceText[index]))
{
return false;
}
}
return i == text.Length;
return true;
}
/// <summary>
@@ -230,46 +267,41 @@ namespace Markdig.Helpers
/// <param name="offset">The offset.</param>
/// <param name="ignoreCase">true if ignore case</param>
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
public int IndexOf(string text, int offset = 0, bool ignoreCase = false)
public readonly int IndexOf(string text, int offset = 0, bool ignoreCase = false)
{
var end = End - text.Length + 1;
if (ignoreCase)
{
for (int i = Start + offset; i <= end; i++)
{
if (MatchLowercase(text, End, i - Start))
{
return i; ;
}
}
}
else
{
for (int i = Start + offset; i <= end; i++)
{
if (Match(text, End, i - Start))
{
return i; ;
}
}
}
return -1;
offset += Start;
int length = End - offset + 1;
if (length <= 0)
return -1;
#if NETCORE
var span = Text.AsSpan(offset, length);
int index = ignoreCase ? span.IndexOf(text, StringComparison.OrdinalIgnoreCase) : span.IndexOf(text);
return index == -1 ? index : index + offset;
#else
return Text.IndexOf(text, offset, length, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
#endif
}
/// <summary>
/// Searches for the specified character within this slice.
/// </summary>
/// <returns>A value >= 0 if the character was found, otherwise &lt; 0</returns>
public int IndexOf(char c)
public readonly int IndexOf(char c)
{
for (int i = Start; i <= End; i++)
{
if (Text[i] == c)
{
return i;
}
}
return -1;
int start = Start;
int length = End - start + 1;
if (length <= 0)
return -1;
#if NETCORE
int index = Text.AsSpan(start, length).IndexOf(c);
return index == -1 ? index : index + start;
#else
return Text.IndexOf(c, start, length);
#endif
}
/// <summary>
@@ -281,7 +313,6 @@ namespace Markdig.Helpers
public bool TrimStart()
{
// Strip leading spaces
var start = Start;
for (; Start <= End; Start++)
{
if (!Text[Start].IsWhitespace())
@@ -289,7 +320,7 @@ namespace Markdig.Helpers
break;
}
}
return start != Start;
return IsEmpty;
}
/// <summary>
@@ -336,31 +367,29 @@ namespace Markdig.Helpers
}
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string ToString()
public readonly override string ToString()
{
if (Text != null && Start <= End)
{
var length = Length;
if (Start == 0 && Text.Length == length)
{
return Text;
}
string text = Text;
int start = Start;
int length = End - start + 1;
return Text.Substring(Start, length);
if (text is null || length <= 0)
{
return string.Empty;
}
return string.Empty;
return text.Substring(start, length);
}
/// <summary>
/// Determines whether this slice is empty or made only of whitespaces.
/// </summary>
/// <returns><c>true</c> if this slice is empty or made only of whitespaces; <c>false</c> otherwise</returns>
public bool IsEmptyOrWhitespace()
public readonly bool IsEmptyOrWhitespace()
{
for (int i = Start; i <= End; i++)
{

View File

@@ -4,7 +4,7 @@
<Description>A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET with 20+ builtin extensions (pipetables, footnotes, definition lists... etc.)</Description>
<Copyright>Alexandre Mutel</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
<VersionPrefix>0.17.1</VersionPrefix>
<VersionPrefix>0.18.1</VersionPrefix>
<Authors>Alexandre Mutel</Authors>
<TargetFrameworks>net35;net40;netstandard2.0;uap10.0;netcoreapp2.1</TargetFrameworks>
<PackageTags>Markdown CommonMark md html md2html</PackageTags>
@@ -13,7 +13,7 @@
<PackageIconUrl>https://raw.githubusercontent.com/lunet-io/markdig/master/img/markdig.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/lunet-io/markdig</PackageProjectUrl>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>7.3</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">

View File

@@ -73,9 +73,18 @@ namespace Markdig
public static string ToHtml(string markdown, MarkdownPipeline pipeline = null)
{
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
var writer = new StringWriter();
ToHtml(markdown, writer, pipeline);
return writer.ToString();
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
pipeline = CheckForSelfPipeline(pipeline, markdown);
var renderer = pipeline.GetCacheableHtmlRenderer();
var document = Parse(markdown, pipeline);
renderer.Render(document);
renderer.Writer.Flush();
string html = renderer.Writer.ToString();
pipeline.ReleaseCacheableHtmlRenderer(renderer);
return html;
}
/// <summary>

View File

@@ -92,8 +92,8 @@ namespace Markdig
.UseDiagrams()
.UseAutoLinks()
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
}
}
/// <summary>
/// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy`
/// </summary>
@@ -421,20 +421,35 @@ namespace Markdig
}
/// <summary>
/// Uses the emoji and smiley extension.
/// Uses the emojis and smileys extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <param name="enableSmiley">Enable smiley in addition to Emoji, <c>true</c> by default.</param>
/// <param name="enableSmileys">Enable smileys in addition to emoji shortcodes, <c>true</c> by default.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, bool enableSmiley = true)
public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, bool enableSmileys = true)
{
if (!pipeline.Extensions.Contains<EmojiExtension>())
{
pipeline.Extensions.Add(new EmojiExtension(enableSmiley));
var emojiMapping = enableSmileys ? EmojiMapping.DefaultEmojisAndSmileysMapping : EmojiMapping.DefaultEmojisOnlyMapping;
pipeline.Extensions.Add(new EmojiExtension(emojiMapping));
}
return pipeline;
}
/// <summary>
/// Uses the emojis and smileys extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <param name="customEmojiMapping">Enable customization of the emojis and smileys mapping.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseEmojiAndSmiley(this MarkdownPipelineBuilder pipeline, EmojiMapping customEmojiMapping)
{
if (!pipeline.Extensions.Contains<EmojiExtension>())
{
pipeline.Extensions.Add(new EmojiExtension(customEmojiMapping));
}
return pipeline;
}
/// <summary>
/// Add rel=nofollow to all links rendered to HTML.
/// </summary>

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
@@ -63,5 +63,42 @@ namespace Markdig
extension.Setup(this, renderer);
}
}
private HtmlRendererCache _rendererCache = null;
internal HtmlRenderer GetCacheableHtmlRenderer()
{
if (_rendererCache is null)
{
_rendererCache = new HtmlRendererCache
{
OnNewInstanceCreated = Setup
};
}
return _rendererCache.Get();
}
internal void ReleaseCacheableHtmlRenderer(HtmlRenderer renderer)
{
_rendererCache.Release(renderer);
}
private sealed class HtmlRendererCache : ObjectCache<HtmlRenderer>
{
public Action<HtmlRenderer> OnNewInstanceCreated;
protected override HtmlRenderer NewInstance()
{
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
OnNewInstanceCreated(renderer);
return renderer;
}
protected override void Reset(HtmlRenderer instance)
{
instance.Reset();
}
}
}
}

View File

@@ -500,6 +500,11 @@ namespace Markdig.Parsers
if (!block.Parser.Close(this, block))
{
block.Parent?.Remove(block);
if (block is LeafBlock leaf)
{
leaf.Lines.Release();
}
}
else
{
@@ -873,7 +878,7 @@ namespace Markdig.Parsers
NewBlocks.Clear();
}
private class BlockParserStateCache : ObjectCache<BlockProcessor>
private sealed class BlockParserStateCache : ObjectCache<BlockProcessor>
{
private readonly BlockProcessor root;

View File

@@ -124,7 +124,9 @@ namespace Markdig.Parsers
}
else
{
infoString = line.ToString().Trim();
var lineCopy = line;
lineCopy.Trim();
infoString = lineCopy.ToString();
}
fenced.Info = HtmlHelper.Unescape(infoString);

View File

@@ -100,7 +100,7 @@ namespace Markdig.Parsers
if (c == '!')
{
c = line.NextChar();
if (c == '-' && line.PeekChar(1) == '-')
if (c == '-' && line.PeekChar() == '-')
{
return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn, startPosition); // group 2
}
@@ -140,7 +140,7 @@ namespace Markdig.Parsers
}
if (
!(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar(1) == '>') || c.IsWhitespace() ||
!(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhitespace() ||
c == '\0'))
{
return BlockState.None;

View File

@@ -192,7 +192,7 @@ namespace Markdig.Parsers
previousLineIndexForSliceOffset = 0;
lineOffsets.Clear();
var text = leafBlock.Lines.ToSlice(lineOffsets);
leafBlock.Lines = new StringLineGroup();
leafBlock.Lines.Release();
int previousStart = -1;

View File

@@ -28,9 +28,7 @@ namespace Markdig.Parsers.Inlines
var c = slice.CurrentChar;
int line;
int column;
var startPosition = processor.GetSourcePosition(slice.Start, out line, out column);
var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column);
bool isImage = false;
if (c == '!')
@@ -49,11 +47,9 @@ namespace Markdig.Parsers.Inlines
// If this is not an image, we may have a reference link shortcut
// so we try to resolve it here
var saved = slice;
string label;
SourceSpan labelSpan;
// If the label is followed by either a ( or a [, this is not a shortcut
if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan))
if (LinkHelper.TryParseLabel(ref slice, out string label, out SourceSpan labelSpan))
{
if (!processor.Document.ContainsLinkReferenceDefinition(label))
{
@@ -97,230 +93,208 @@ namespace Markdig.Parsers.Inlines
private bool ProcessLinkReference(InlineProcessor state, string label, bool isShortcut, SourceSpan labelSpan, LinkDelimiterInline parent, int endPosition)
{
bool isValidLink = false;
LinkReferenceDefinition linkRef;
if (state.Document.TryGetLinkReferenceDefinition(label, out linkRef))
if (!state.Document.TryGetLinkReferenceDefinition(label, out LinkReferenceDefinition linkRef))
{
Inline link = null;
// Try to use a callback directly defined on the LinkReferenceDefinition
if (linkRef.CreateLinkInline != null)
{
link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild);
}
return false;
}
// Create a default link if the callback was not found
if (link == null)
Inline link = null;
// Try to use a callback directly defined on the LinkReferenceDefinition
if (linkRef.CreateLinkInline != null)
{
link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild);
}
// Create a default link if the callback was not found
if (link == null)
{
// Inline Link
link = new LinkInline()
{
// Inline Link
link = new LinkInline()
Url = HtmlHelper.Unescape(linkRef.Url),
Title = HtmlHelper.Unescape(linkRef.Title),
Label = label,
LabelSpan = labelSpan,
UrlSpan = linkRef.UrlSpan,
IsImage = parent.IsImage,
IsShortcut = isShortcut,
Reference = linkRef,
Span = new SourceSpan(parent.Span.Start, endPosition),
Line = parent.Line,
Column = parent.Column,
};
}
if (link is ContainerInline containerLink)
{
var child = parent.FirstChild;
if (child == null)
{
child = new LiteralInline()
{
Url = HtmlHelper.Unescape(linkRef.Url),
Title = HtmlHelper.Unescape(linkRef.Title),
Label = label,
LabelSpan = labelSpan,
UrlSpan = linkRef.UrlSpan,
IsImage = parent.IsImage,
IsShortcut = isShortcut,
Reference = linkRef,
Span = new SourceSpan(parent.Span.Start, endPosition),
Content = StringSlice.Empty,
IsClosed = true,
// Not exact but we leave it like this
Span = parent.Span,
Line = parent.Line,
Column = parent.Column,
};
containerLink.AppendChild(child);
}
var containerLink = link as ContainerInline;
if (containerLink != null)
else
{
var child = parent.FirstChild;
if (child == null)
// Insert all child into the link
while (child != null)
{
child = new LiteralInline()
{
Content = StringSlice.Empty,
IsClosed = true,
// Not exact but we leave it like this
Span = parent.Span,
Line = parent.Line,
Column = parent.Column,
};
var next = child.NextSibling;
child.Remove();
containerLink.AppendChild(child);
}
else
{
// Insert all child into the link
while (child != null)
{
var next = child.NextSibling;
child.Remove();
containerLink.AppendChild(child);
child = next;
}
child = next;
}
}
link.IsClosed = true;
// Process emphasis delimiters
state.PostProcessInlines(0, link, null, false);
state.Inline = link;
isValidLink = true;
}
//else
//{
// // Else output a literal, leave it opened as we may have literals after
// // that could be append to this one
// var literal = new LiteralInline()
// {
// ContentBuilder = processor.StringBuilders.Get().Append('[').Append(label).Append(']')
// };
// processor.Inline = literal;
//}
return isValidLink;
link.IsClosed = true;
// Process emphasis delimiters
state.PostProcessInlines(0, link, null, false);
state.Inline = link;
return true;
}
private bool TryProcessLinkOrImage(InlineProcessor inlineState, ref StringSlice text)
{
LinkDelimiterInline openParent = null;
foreach (var parent in inlineState.Inline.FindParentOfType<LinkDelimiterInline>())
LinkDelimiterInline openParent = inlineState.Inline.FirstParentOfType<LinkDelimiterInline>();
if (openParent is null)
{
openParent = parent;
break;
}
if (openParent != null)
{
// If we do find one, but its not active,
// we remove the inactive delimiter from the stack,
// and return a literal text node ].
if (!openParent.IsActive)
{
inlineState.Inline = new LiteralInline()
{
Content = new StringSlice("["),
Span = openParent.Span,
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(inlineState.Inline);
return false;
}
// If we find one and its active,
// then we parse ahead to see if we have
// an inline link/image, reference link/image,
// compact reference link/image,
// or shortcut reference link/image
var parentDelimiter = openParent.Parent;
var savedText = text;
switch (text.CurrentChar)
{
case '(':
string url;
string title;
SourceSpan linkSpan;
SourceSpan titleSpan;
if (LinkHelper.TryParseInlineLink(ref text, out url, out title, out linkSpan, out titleSpan))
{
// Inline Link
var link = new LinkInline()
{
Url = HtmlHelper.Unescape(url),
Title = HtmlHelper.Unescape(title),
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start -1)),
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(link);
// Notifies processor as we are creating an inline locally
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.
// (This will prevent us from getting links within links.)
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
link.IsClosed = true;
return true;
}
text = savedText;
goto default;
default:
var labelSpan = SourceSpan.Empty;
string label = null;
bool isLabelSpanLocal = true;
bool isShortcut = false;
// Handle Collapsed links
if (text.CurrentChar == '[')
{
if (text.PeekChar(1) == ']')
{
label = openParent.Label;
labelSpan = openParent.LabelSpan;
isLabelSpanLocal = false;
text.NextChar(); // Skip [
text.NextChar(); // Skip ]
}
}
else
{
label = openParent.Label;
isShortcut = true;
}
if (label != null || LinkHelper.TryParseLabel(ref text, true, out label, out labelSpan))
{
if (isLabelSpanLocal)
{
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
}
if (ProcessLinkReference(inlineState, label, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1)))
{
// Remove the open parent
openParent.Remove();
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
}
else
{
return false;
}
return true;
}
break;
}
// We have a nested [ ]
// firstParent.Remove();
// The opening [ will be transformed to a literal followed by all the children of the [
var literal = new LiteralInline()
{
Span = openParent.Span,
Content = new StringSlice(openParent.IsImage ? "![" : "[")
};
inlineState.Inline = openParent.ReplaceBy(literal);
return false;
}
// If we do find one, but its not active,
// we remove the inactive delimiter from the stack,
// and return a literal text node ].
if (!openParent.IsActive)
{
inlineState.Inline = new LiteralInline()
{
Content = new StringSlice("["),
Span = openParent.Span,
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(inlineState.Inline);
return false;
}
// If we find one and its active,
// then we parse ahead to see if we have
// an inline link/image, reference link/image,
// compact reference link/image,
// or shortcut reference link/image
var parentDelimiter = openParent.Parent;
var savedText = text;
if (text.CurrentChar == '(')
{
if (LinkHelper.TryParseInlineLink(ref text, out string url, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan))
{
// Inline Link
var link = new LinkInline()
{
Url = HtmlHelper.Unescape(url),
Title = HtmlHelper.Unescape(title),
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(link);
// Notifies processor as we are creating an inline locally
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.
// (This will prevent us from getting links within links.)
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
link.IsClosed = true;
return true;
}
text = savedText;
}
var labelSpan = SourceSpan.Empty;
string label = null;
bool isLabelSpanLocal = true;
bool isShortcut = false;
// Handle Collapsed links
if (text.CurrentChar == '[')
{
if (text.PeekChar() == ']')
{
label = openParent.Label;
labelSpan = openParent.LabelSpan;
isLabelSpanLocal = false;
text.NextChar(); // Skip [
text.NextChar(); // Skip ]
}
}
else
{
label = openParent.Label;
isShortcut = true;
}
if (label != null || LinkHelper.TryParseLabel(ref text, true, out label, out labelSpan))
{
if (isLabelSpanLocal)
{
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
}
if (ProcessLinkReference(inlineState, label, isShortcut, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1)))
{
// Remove the open parent
openParent.Remove();
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
return true;
}
else if (text.CurrentChar != ']' && text.CurrentChar != '[')
{
return false;
}
}
// We have a nested [ ]
// firstParent.Remove();
// The opening [ will be transformed to a literal followed by all the children of the [
var literal = new LiteralInline()
{
Span = openParent.Span,
Content = new StringSlice(openParent.IsImage ? "![" : "[")
};
inlineState.Inline = openParent.ReplaceBy(literal);
return false;
}

View File

@@ -28,6 +28,8 @@ namespace Markdig.Parsers
private readonly ProcessDocumentDelegate documentProcessed;
private readonly bool preciseSourceLocation;
private readonly int roughLineCountEstimate;
private LineReader lineReader;
/// <summary>
@@ -42,6 +44,8 @@ namespace Markdig.Parsers
{
if (text == null) throw new ArgumentNullException(nameof(text));
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
roughLineCountEstimate = text.Length / 40;
text = FixupZero(text);
lineReader = new LineReader(text);
preciseSourceLocation = pipeline.PreciseSourceLocation;
@@ -82,13 +86,16 @@ namespace Markdig.Parsers
}
/// <summary>
/// Parses the current <see cref="Reader"/> into a Markdown <see cref="MarkdownDocument"/>.
/// Parses the current <see cref="lineReader"/> into a Markdown <see cref="MarkdownDocument"/>.
/// </summary>
/// <returns>A document instance</returns>
private MarkdownDocument Parse()
{
if (preciseSourceLocation)
document.LineStartIndexes = new List<int>();
{
// Save some List resizing allocations
document.LineStartIndexes = new List<int>(Math.Min(512, roughLineCountEstimate));
}
ProcessBlocks();
ProcessInlines();
@@ -127,7 +134,7 @@ namespace Markdig.Parsers
return text.Replace('\0', CharHelper.ZeroSafeChar);
}
private class ContainerItemCache : DefaultObjectCache<ContainerItem>
private sealed class ContainerItemCache : DefaultObjectCache<ContainerItem>
{
protected override void Reset(ContainerItem instance)
{

View File

@@ -70,7 +70,9 @@ namespace Markdig.Renderers
public abstract class TextRendererBase<T> : TextRendererBase where T : TextRendererBase<T>
{
private bool previousWasLine;
#if !NETCORE
private char[] buffer;
#endif
private readonly List<string> indents;
/// <summary>
@@ -79,12 +81,29 @@ namespace Markdig.Renderers
/// <param name="writer">The writer.</param>
protected TextRendererBase(TextWriter writer) : base(writer)
{
#if !NETCORE
buffer = new char[1024];
#endif
// We assume that we are starting as if we had previously a newline
previousWasLine = true;
indents = new List<string>();
}
internal void Reset()
{
if (Writer is StringWriter stringWriter)
{
stringWriter.GetStringBuilder().Length = 0;
}
else
{
throw new InvalidOperationException("Cannot reset this TextWriter instance");
}
previousWasLine = true;
indents.Clear();
}
/// <summary>
/// Ensures a newline.
/// </summary>
@@ -193,6 +212,10 @@ namespace Markdig.Renderers
WriteIndent();
previousWasLine = false;
#if NETCORE
Writer.Write(content.AsSpan(offset, length));
#else
if (offset == 0 && content.Length == length)
{
Writer.Write(content);
@@ -210,6 +233,7 @@ namespace Markdig.Renderers
Writer.Write(buffer, 0, length);
}
}
#endif
return (T) this;
}

View File

@@ -189,12 +189,17 @@ namespace Markdig.Syntax
{
get
{
if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index));
return children[index];
var array = children;
if ((uint)index >= (uint)array.Length || index >= Count)
{
ThrowHelper.ThrowIndexOutOfRangeException();
return null;
}
return array[index];
}
set
{
if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index));
if ((uint)index >= (uint)Count) ThrowHelper.ThrowIndexOutOfRangeException();
children[index] = value;
}
}

View File

@@ -1,9 +1,11 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespace Markdig.Syntax.Inlines
@@ -94,8 +96,20 @@ namespace Markdig.Syntax.Inlines
/// <returns>An enumeration of T</returns>
public IEnumerable<T> FindDescendants<T>() where T : Inline
{
// Fast-path an empty container to avoid allocating a Stack
if (LastChild == null) yield break;
if (FirstChild is null)
{
return ArrayHelper<T>.Empty;
}
else
{
return FindDescendantsInternal<T>();
}
}
internal IEnumerable<T> FindDescendantsInternal<T>() where T : MarkdownObject
{
#if !UAP
Debug.Assert(typeof(T).IsSubclassOf(typeof(Inline)));
#endif
Stack<Inline> stack = new Stack<Inline>();

View File

@@ -215,15 +215,28 @@ namespace Markdig.Syntax.Inlines
var inline = this;
while (inline != null)
{
var delimiter = inline as T;
if (delimiter != null)
if (inline is T inlineOfT)
{
yield return delimiter;
yield return inlineOfT;
}
inline = inline.Parent;
}
}
internal T FirstParentOfType<T>() where T : Inline
{
var inline = this;
while (inline != null)
{
if (inline is T inlineOfT)
{
return inlineOfT;
}
inline = inline.Parent;
}
return null;
}
public Inline FindBestParent()
{
var current = this;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics;
@@ -51,7 +51,7 @@ namespace Markdig.Syntax
{
if (Lines.Lines == null)
{
Lines = new StringLineGroup(4);
Lines = new StringLineGroup(4, ProcessInlines);
}
var stringLine = new StringLine(ref slice, line, column, sourceLinePosition);
@@ -64,12 +64,9 @@ namespace Markdig.Syntax
{
// We need to expand tabs to spaces
var builder = StringBuilderCache.Local();
for (int i = column; i < CharHelper.AddTab(column); i++)
{
builder.Append(' ');
}
builder.Append(' ', CharHelper.AddTab(column) - column);
builder.Append(slice.Text, slice.Start + 1, slice.Length - 1);
stringLine.Slice = new StringSlice(builder.ToString());
stringLine.Slice = new StringSlice(builder.GetStringAndReset());
Lines.Add(ref stringLine);
}
}

View File

@@ -3,6 +3,8 @@
// See the license.txt file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using Markdig.Helpers;
using Markdig.Syntax.Inlines;
namespace Markdig.Syntax
@@ -20,10 +22,6 @@ namespace Markdig.Syntax
/// <returns>An iteration over the descendant elements</returns>
public static IEnumerable<MarkdownObject> Descendants(this MarkdownObject markdownObject)
{
// Fast-path an object with no children to avoid allocating Stack objects
if (!(markdownObject is ContainerBlock) && !(markdownObject is ContainerInline)) yield break;
// TODO: A single Stack<(MarkdownObject block, bool push)> when ValueTuples are available
Stack<MarkdownObject> stack = new Stack<MarkdownObject>();
Stack<bool> pushStack = new Stack<bool>();
@@ -66,7 +64,51 @@ namespace Markdig.Syntax
}
/// <summary>
/// Iterates over the descendant elements for the specified markdown <see cref="Inline" /> element and filters by the type {T}.
/// Iterates over the descendant elements for the specified markdown element, including <see cref="Block"/> and <see cref="Inline"/> and filters by the type <typeparamref name="T"/>.
/// <para>The descendant elements are returned in DFS-like order.</para>
/// </summary>
/// <typeparam name="T">Type to use for filtering the descendants</typeparam>
/// <param name="markdownObject">The markdown object.</param>
/// <returns>An iteration over the descendant elements</returns>
public static IEnumerable<T> Descendants<T>(this MarkdownObject markdownObject) where T : MarkdownObject
{
#if UAP
foreach (MarkdownObject descendant in markdownObject.Descendants())
{
if (descendant is T descendantT)
{
yield return descendantT;
}
}
#else
if (typeof(T).IsSubclassOf(typeof(Block)))
{
if (markdownObject is ContainerBlock containerBlock && containerBlock.Count > 0)
{
return BlockDescendantsInternal<T>(containerBlock);
}
}
else // typeof(T).IsSubclassOf(typeof(Inline)))
{
if (markdownObject is ContainerBlock containerBlock)
{
if (containerBlock.Count > 0)
{
return InlineDescendantsInternal<T>(containerBlock);
}
}
else if (markdownObject is ContainerInline containerInline && containerInline.FirstChild != null)
{
return containerInline.FindDescendantsInternal<T>();
}
}
return ArrayHelper<T>.Empty;
#endif
}
/// <summary>
/// Iterates over the descendant elements for the specified markdown <see cref="Inline" /> element and filters by the type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type to use for filtering the descendants</typeparam>
/// <param name="inline">The inline markdown object.</param>
@@ -77,7 +119,7 @@ namespace Markdig.Syntax
=> inline.FindDescendants<T>();
/// <summary>
/// Iterates over the descendant elements for the specified markdown <see cref="Block" /> element and filters by the type {T}.
/// Iterates over the descendant elements for the specified markdown <see cref="Block" /> element and filters by the type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type to use for filtering the descendants</typeparam>
/// <param name="block">The markdown object.</param>
@@ -86,8 +128,21 @@ namespace Markdig.Syntax
/// </returns>
public static IEnumerable<T> Descendants<T>(this ContainerBlock block) where T : Block
{
// Fast-path an empty container to avoid allocating a Stack
if (block.Count == 0) yield break;
if (block != null && block.Count > 0)
{
return BlockDescendantsInternal<T>(block);
}
else
{
return ArrayHelper<T>.Empty;
}
}
private static IEnumerable<T> BlockDescendantsInternal<T>(ContainerBlock block) where T : MarkdownObject
{
#if !UAP
Debug.Assert(typeof(T).IsSubclassOf(typeof(Block)));
#endif
Stack<Block> stack = new Stack<Block>();
@@ -115,5 +170,20 @@ namespace Markdig.Syntax
}
}
}
private static IEnumerable<T> InlineDescendantsInternal<T>(ContainerBlock block) where T : MarkdownObject
{
#if !UAP
Debug.Assert(typeof(T).IsSubclassOf(typeof(Inline)));
#endif
foreach (MarkdownObject descendant in block.Descendants())
{
if (descendant is T descendantT)
{
yield return descendantT;
}
}
}
}
}