Compare commits

...

42 Commits

Author SHA1 Message Date
Alexandre Mutel
f64ac47841 Bump to 0.14.9 2018-01-15 10:12:42 +01:00
Alexandre Mutel
c29b7d2942 Merge pull request #195 from leotsarev/remove-mailto
AutoLinkParser should to remove mailto: in outputted text
2018-01-10 11:17:34 +01:00
Leonid Tsarev
0f7e3b8c52 AutoLinkParser should to remove mailto: in outputted text 2018-01-09 11:48:06 +03:00
Alexandre Mutel
6dff16a612 Merge pull request #191 from leotsarev/improve-test-names
Improve discoverability of test in VS test runner
2018-01-08 20:50:43 +01:00
Alexandre Mutel
8e15c8bc6a Merge pull request #193 from leotsarev/support-vk-and-yandex-music
Added yandex.music & ok.ru support to MediaLinkExtension
2018-01-08 18:06:32 +01:00
Leonid Tsarev
558db1fd70 Support odnoklassniki.ru (meaning: classmates, top 2 social network in Russia) 2018-01-08 19:30:23 +03:00
Leonid Tsarev
796b143316 Add support for yandex music 2018-01-08 19:14:01 +03:00
Leonid Tsarev
3402805ebb refactor MediaLinkExtension to make it pluggable 2018-01-08 18:48:09 +03:00
Leonid Tsarev
c3600c2ba5 Merge branch 'master' into improve-test-names 2018-01-08 18:09:54 +03:00
Leonid Tsarev
4ba98f594a Merge branch 'master' into improve-test-names 2018-01-08 18:08:47 +03:00
Leonid Tsarev
544a64e5c9 Improve discoverability of test in VS test runner 2018-01-08 18:05:19 +03:00
Alexandre Mutel
f17320702a Merge pull request #189 from markheath/patch-1
Media link extension to support MP3 by default
2018-01-03 09:08:51 +01:00
Alexandre Mutel
d883694ac4 Merge pull request #186 from tthiery/normalize-autolinks
Add Normalization Support for AutoLinks
2018-01-03 09:08:08 +01:00
Mark Heath
42fba26ea2 Media link extension to support MP3 by default
I presume MP3 wasn't intentionally left out this list?
2018-01-01 13:50:52 +00:00
T. Thiery
595dacf213 Add Normalization Support for AutoLinks (including Options) 2017-12-24 12:21:23 +01:00
T. Thiery
75adfa3fe8 Add Normalization Tests for AutoLinks 2017-12-24 12:20:26 +01:00
Alexandre Mutel
0bb8139450 Merge pull request #185 from tthiery/normalize-jiralinks
Add Normalization Support for JIRA Links
2017-12-17 20:25:59 +01:00
T. Thiery
60784901b2 Add Normalization Support for JIRA Links 2017-12-17 17:00:55 +01:00
Alexandre Mutel
5020fdd3b1 Bump to 0.14.8 2017-12-05 09:36:48 +01:00
Alexandre Mutel
bd4ea56d2a Fix potential StackOverflow exception when processing deep nested | delimiters (#179) 2017-12-05 09:34:59 +01:00
Alexandre Mutel
fbc8a4116c Bump to 0.14.7 2017-11-25 14:17:23 +01:00
Alexandre Mutel
9af318d334 Merge pull request #175 from terryjintry/fixautolink
fix autolink attribute
2017-11-24 09:29:44 +01:00
Terry Jin
88e561c3ae fix autolink attribute 2017-11-24 14:34:44 +08:00
Alexandre Mutel
1ca82eb058 Bump to 0.14.6 2017-11-21 07:36:14 +01:00
Alexandre Mutel
66007c6bbf Add TestLink example (#171) 2017-11-21 07:35:17 +01:00
Alexandre Mutel
14d4286fd7 Merge pull request #170 from yishengjin1413/master
fix yaml frontmatter issue when ending with a empty line
2017-11-20 08:26:47 +01:00
Terry Jin
58b1a48f5b fix yaml frontmatter issue when ending with a empty line 2017-11-20 15:11:16 +08:00
Alexandre Mutel
5e1eaf8590 Bump to 0.14.5 2017-11-18 17:08:21 +01:00
Alexandre Mutel
c946295d96 Fix link to changelog.md in Markdig.csproj 2017-11-18 17:08:11 +01:00
Alexandre Mutel
97cbf11f8b Bump to 0.14.4 2017-11-18 11:29:39 +01:00
Alexandre Mutel
8d4394e7c6 Fix link conflict between a link to an image definition and heading auto-identifiers (#159) 2017-11-18 11:17:06 +01:00
Alexandre Mutel
fbdb8cf063 Better handle YAML frontmatter in case the opening --- is never actually closed (#160) 2017-11-18 10:55:16 +01:00
Alexandre Mutel
964538ec79 Add support for GFM autolinks (#165, #169) 2017-11-17 21:28:27 +01:00
Alexandre Mutel
9a30883e2a Add support for compatible github auto-identifiers for headings 2017-11-10 20:42:53 +01:00
Alexandre Mutel
6408705f82 Return an empty string for / on markdig webapi 2017-11-10 10:59:27 +01:00
Alexandre Mutel
523582e588 Fix bug when a thematic break is inside a fenced code block inside a pending list (#164) 2017-11-09 09:08:51 +01:00
Alexandre Mutel
aaff022e7c Merge pull request #161 from tthiery/normalize-tasklists
Add Normalization Support for Task Lists
2017-11-04 23:09:14 +01:00
T. Thiery
37eb6aa529 Add Normalization Support for Task Lists 2017-11-04 23:01:01 +01:00
Alexandre Mutel
9b7356b05e Remove local project 2017-11-03 14:22:05 +01:00
Alexandre Mutel
33d3bc1330 Upgrade WebApp to .NET core app 2.0 and add ApplicationInsights 2017-11-03 14:19:32 +01:00
Alexandre Mutel
07a2980d5b Bump to 0.14.3 2017-11-01 07:44:41 +01:00
Alexandre Mutel
2d8872f2a1 Make EmojiExtension.EnableSmiley public 2017-11-01 07:44:30 +01:00
43 changed files with 1880 additions and 1077 deletions

118
changelog.md Normal file
View File

@@ -0,0 +1,118 @@
# Changelog
## 0.14.9 (15 Jan 2018)
- AutoLinkParser should to remove mailto: in outputted text ([(PR #195)](https://github.com/lunet-io/markdig/pull/195))
- Add support for `music.yandex.ru` and `ok.ru` for MediaLinks extension ([(PR #193)](https://github.com/lunet-io/markdig/pull/193))
## 0.14.8 (05 Dec 2017)
- Fix potential StackOverflow exception when processing deep nested `|` delimiters (#179)
## 0.14.7 (25 Nov 2017)
- Fix autolink attached attributes not being displayed properly (#175)
## 0.14.6 (21 Nov 2017)
- Fix yaml frontmatter issue when ending with a empty line (#170)
## 0.14.5 (18 Nov 2017)
- Fix changelog link from nuget package
## 0.14.4 (18 Nov 2017)
- Add changelog.md
- Fix bug when a thematic break is inside a fenced code block inside a pending list (#164)
- Add support for GFM autolinks (#165, #169)
- Better handle YAML frontmatter in case the opening `---` is never actually closed (#160)
- Fix link conflict between a link to an image definition and heading auto-identifiers (#159)
## 0.14.3
- Make EmojiExtension.EnableSmiley public
## 0.14.2
- Fix issue with emphasis preceded/followed by an HTML entity (#157)
- Add support for link reference definitions for Normalize renderer (#155)
- Add option to disable smiley parsing in EmojiAndSmiley extension
## 0.14.1
- Fix crash in Markdown.Normalize to handle HtmlBlock correctly
- Add better handling of bullet character for lists in Markdown.Normalize
## 0.14.0
- Add Markdown.ToPlainText, Add option HtmlRenderer.EnableHtmlForBlock.
- Add Markdown.Normalize, to allow to normalize a markdown document. Add NormalizeRenderer, to render a MarkdownDocument back to markdown.
## 0.13.4
- Add support for single table header row without a table body rows (#141)
- ADd support for `nomnoml` diagrams
## 0.13.3
- Add support for Pandoc YAML frontmatter (#138)
## 0.13.2
- Add support for UAP10.0 (#137)
## 0.13.1
- Fix indenting issue after a double digit list block using a tab (#134)
## 0.13.0
- Update to latest CommonMark specs 0.28
## 0.12.3
- Fix issue with HTML blocks for heading h2,h3,h4,h5,h6 that were not correctly identified as HTML blocks as per CommonMark spec
## 0.12.2
- Fix issue with generic attributes used just before a pipe table (issue #121)
## 0.12.1
- Fix issue with media links extension when a URL to video is used, an unexpected closing `</iframe>` was inserted (issue #119)
## 0.12.0
- Add new extension JiraLink support (thanks to @clarkd)
- Fix issue in html attributes not parsing correctly properties (thanks to @meziantou)
- Fix issues detected by an automatic static code analysis tool
## 0.11.0
- Fix issue with math extension and $$ block parsing not handling correctly beginning of a $$ as a inline math instead (issue #107)
- Fix issue with custom attributes for emphasis
- Add support for new special custom arrows emoji (`->` `<-` `<->` `<=` `=>` `<==>`)
## 0.10.7
- Fix issue when an url ends by a dot `.`
## 0.10.6
- Fix emphasis with HTML entities
## 0.10.5
- Several minor fixes
## 0.10.4
- Fix issue with autolinks
- Normalize number of columns for tables
## 0.10.3
- Fix issue with pipetables shifting a cell to a new column (issue #73)
## 0.10.2
- Fix exception when trying to urlize an url with an unicode character outside the supported range by NormD (issue #75)
## 0.10.1
- Update to latest CommonMark specs
- Fix source span for LinkReferenceDefinition
## 0.10.0
- Breaking change of the IMarkdownExtension to allow to receive the MarkdownPipeline for the renderers setup
## 0.9.1
- Fix regression bug with conflicts between autolink extension and html inline/regular links
## 0.9.0
- Add new Autolink extension
## 0.8.5
- Allow to force table column alignment to left
## 0.8.4
- Fix issue when calculating the span of an indented code block within a list. Make sure to include first whitespace on the line
## 0.8.3
- fix NullReferenceException with Gridtables extension when a single `+` is entered on a line
## 0.8.2
- fix potential cast exception with Abreviation extension and empty literals
## 0.8.1
- new extension to disable URI escaping for non-US-ASCII characters to workaround a bug in Edge/IE
- Fix an issue with abbreviations with left/right multiple non-punctuation/space characters
## 0.8.0
- Update to latest CommonMark specs
- Fix empty literal
- Add YAML frontmatter extension
## 0.7.5
- several bug fixes (pipe tables, disable HTML, special attributes, inline maths, abbreviations...)
- add support for rowspan in grid tables
## 0.7.4
- Fix bug with strong emphasis starting at the beginning of a line
## 0.7.3
- Fix threading issue with pipeline
## 0.7.2
- Fix rendering of table colspan with non english locale
- Fix grid table colspan parsing
- Add nofollow extension for links
## 0.7.1
- Fix issue in smarty pants which could lead to an InvalidCastException
- Update parsers to latest CommonMark specs
## 0.7.0
- Update to latest NETStandard.Library 1.6.0
- Fix issue with digits in auto-identifier extension
- Fix incorrect start of span calculated for code indented blocks
## 0.6.2
- Handle latest CommonMark specs for corner cases for emphasis (See https://talk.commonmark.org/t/emphasis-strong-emphasis-corner-cases/2123/1 )
## 0.6.1:
- Fix issue with autoidentifier extension overriding manual HTML attributes id on headings
## 0.6.0
- Fix conflicts between PipeTables and SmartyPants extensions
- Add SelfPipeline extension

View File

@@ -96,3 +96,14 @@ The text of the link can be changed:
<p><a href="#this-is-a-heading">With a new text</a></p>
<h1 id="this-is-a-heading">This is a heading</h1>
````````````````````````````````
An autoidentifier should not conflict with an existing link:
```````````````````````````````` example
![scenario image][scenario]
## Scenario
[scenario]: ./scenario.png
.
<p><img src="./scenario.png" alt="scenario image" /></p>
<h2 id="scenario">Scenario</h2>
````````````````````````````````

View File

@@ -19,7 +19,7 @@ And a plain www.google.com
.
<p>This is a <a href="http://www.google.com">http://www.google.com</a> URL and <a href="https://www.google.com">https://www.google.com</a>
This is a <a href="ftp://test.com">ftp://test.com</a>
And a <a href="mailto:email@toto.com">mailto:email@toto.com</a>
And a <a href="mailto:email@toto.com">email@toto.com</a>
And a plain <a href="http://www.google.com">www.google.com</a></p>
````````````````````````````````
@@ -76,3 +76,67 @@ Check **http://www.a.com** or __http://www.b.com__
.
<p>Check <strong><a href="http://www.a.com">http://www.a.com</a></strong> or <strong><a href="http://www.b.com">http://www.b.com</a></strong></p>
````````````````````````````````
### GFM Support
Extract from [GFM Autolinks extensions specs](https://github.github.com/gfm/#autolinks-extension-)
```````````````````````````````` example
www.commonmark.org
.
<p><a href="http://www.commonmark.org">www.commonmark.org</a></p>
````````````````````````````````
```````````````````````````````` example
Visit www.commonmark.org/help for more information.
.
<p>Visit <a href="http://www.commonmark.org/help">www.commonmark.org/help</a> for more information.</p>
````````````````````````````````
```````````````````````````````` example
Visit www.commonmark.org.
Visit www.commonmark.org/a.b.
.
<p>Visit <a href="http://www.commonmark.org">www.commonmark.org</a>.</p>
<p>Visit <a href="http://www.commonmark.org/a.b">www.commonmark.org/a.b</a>.</p>
````````````````````````````````
```````````````````````````````` example
www.google.com/search?q=Markup+(business)
(www.google.com/search?q=Markup+(business))
.
<p><a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a></p>
<p>(<a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a>)</p>
````````````````````````````````
```````````````````````````````` example
www.google.com/search?q=commonmark&hl=en
www.google.com/search?q=commonmark&hl;
.
<p><a href="http://www.google.com/search?q=commonmark&amp;hl=en">www.google.com/search?q=commonmark&amp;hl=en</a></p>
<p><a href="http://www.google.com/search?q=commonmark">www.google.com/search?q=commonmark</a>&amp;hl;</p>
````````````````````````````````
```````````````````````````````` example
www.commonmark.org/he<lp
.
<p><a href="http://www.commonmark.org/he">www.commonmark.org/he</a>&lt;lp</p>
````````````````````````````````
```````````````````````````````` example
http://commonmark.org
(Visit https://encrypted.google.com/search?q=Markup+(business))
Anonymous FTP is available at ftp://foo.bar.baz.
.
<p><a href="http://commonmark.org">http://commonmark.org</a></p>
<p>(Visit <a href="https://encrypted.google.com/search?q=Markup+(business)">https://encrypted.google.com/search?q=Markup+(business)</a>)</p>
<p>Anonymous FTP is available at <a href="ftp://foo.bar.baz">ftp://foo.bar.baz</a>.</p>
````````````````````````````````

View File

@@ -12,8 +12,14 @@ Allows to embed audio/video links to popular website:
![Video2](https://vimeo.com/8607834)
![Video3](https://sample.com/video.mp4)
![Audio4](https://music.yandex.ru/album/411845/track/4402274)
![Video5](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://player.vimeo.com/video/8607834" width="500" height="281" 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>
````````````````````````````````

File diff suppressed because it is too large Load Diff

View File

@@ -148,7 +148,7 @@ private class Spec
public string Name
{
get { return string.Format("Example{0:000}", Example); }
get { return string.Format("{0}_Example{1:000}", SecHeadingCompact, Example); }
}
}

View File

@@ -47,12 +47,25 @@ It can end with three dots `...`:
```````````````````````````````` example
---
this: is a frontmatter
...
This is a text
.
<p>This is a text</p>
````````````````````````````````
If the end front matter marker (`...` or `---`) is not present, it will render the `---` has a `<hr>`:
```````````````````````````````` example
---
this: is a frontmatter
This is a text
.
<hr />
<p>this: is a frontmatter
This is a text</p>
````````````````````````````````
It expects exactly three dots `...`:
```````````````````````````````` example
@@ -61,6 +74,10 @@ this: is a frontmatter
....
This is a text
.
<hr />
<p>this: is a frontmatter
....
This is a text</p>
````````````````````````````````
Front matter ends with the first line containing three dots `...` or three dashes `...`:

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.
@@ -39,7 +39,7 @@ namespace Markdig.Tests
{
var text = new StringSlice(uri);
string link;
Assert.True(LinkHelper.TryParseUrl(ref text, out link));
Assert.True(LinkHelper.TryParseUrl(ref text, out link, true));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual('.', text.CurrentChar);
}

View File

@@ -396,6 +396,43 @@ This is a last line";
AssertNormalizeNoTrim(input);
}
[Test]
public void TaskLists()
{
AssertNormalizeNoTrim("- [X] This is done");
AssertNormalizeNoTrim("- [x] This is done",
"- [X] This is done");
AssertNormalizeNoTrim("- [ ] This is not done");
// ignore
AssertNormalizeNoTrim("[x] This is not a task list");
AssertNormalizeNoTrim("[ ] This is not a task list");
}
[Test]
public void JiraLinks()
{
AssertNormalizeNoTrim("FOO-1234");
AssertNormalizeNoTrim("AB-1");
AssertNormalizeNoTrim("**Hello World AB-1**");
}
[Test]
public void AutoLinks()
{
AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from [http://example.com/foo](http://example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from [www.example.com/foo](http://www.example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from ftp://example.com", "Hello from [ftp://example.com](ftp://example.com)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from [hello@example.com](mailto:hello@example.com)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from http://example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, });
AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from http://www.example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, });
AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from mailto:hello@example.com", new NormalizeOptions() { ExpandAutoLinks = false, });
}
private static void AssertSyntax(string expected, MarkdownObject syntax)
{
var writer = new StringWriter();
@@ -425,7 +462,13 @@ This is a last line";
input = NormText(input, trim);
expected = NormText(expected, trim);
var result = Markdown.Normalize(input, options);
var pipeline = new MarkdownPipelineBuilder()
.UseAutoLinks()
.UseJiraLinks(new Extensions.JiraLinks.JiraLinkOptions("https://jira.example.com"))
.UseTaskLists()
.Build();
var result = Markdown.Normalize(input, options, pipeline: pipeline);
result = NormText(result, trim);
Console.WriteLine("```````````````````Source");

View File

@@ -1,5 +1,5 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// 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;
@@ -19,6 +19,25 @@ namespace Markdig.Tests
TestSpec(markdownText, "<p><em>Unlimited-Fun®</em>®</p>");
}
[Test]
public void TestThematicInsideCodeBlockInsideList()
{
var input = @"1. In the :
```
Id DisplayName Description
-- ----------- -----------
62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
```";
TestSpec(input, @"<ol>
<li><p>In the :</p>
<pre><code>Id DisplayName Description
-- ----------- -----------
62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
</code></pre></li>
</ol>");
}
public static void TestSpec(string inputText, string expectedOutputText, string extensions = null)
{
foreach (var pipeline in GetPipeline(extensions))

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;
@@ -12,6 +12,14 @@ namespace Markdig.Tests
[TestFixture]
public class TestPlayParser
{
[Test]
public void TestLink()
{
var doc = Markdown.Parse("There is a ![link](/yoyo)");
var link = doc.Descendants<ParagraphBlock>().SelectMany(x => x.Inline.Descendants<LinkInline>()).FirstOrDefault(l => l.IsImage);
Assert.AreEqual("/yoyo", link?.Url);
}
[Test]
public void TestListBug2()
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
@@ -6,6 +6,13 @@ namespace Markdig.WebApp
{
public class ApiController : Controller
{
[HttpGet()]
[Route("")]
public string Empty()
{
return string.Empty;
}
// GET api/to_html?text=xxx&extensions=advanced
[Route("api/to_html")]
[HttpGet()]

View File

@@ -0,0 +1,7 @@
{
"ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider",
"Version": "8.9.809.2",
"GettingStartedDocument": {
"Uri": "https://go.microsoft.com/fwlink/?LinkID=798432"
}
}

View File

@@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.0</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<AssemblyName>Markdig.WebApp</AssemblyName>
<OutputType>Exe</OutputType>
<PackageId>Markdig.WebApp</PackageId>
<RuntimeFrameworkVersion>1.0.4</RuntimeFrameworkVersion>
<PackageTargetFallback>$(PackageTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8</PackageTargetFallback>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
<ApplicationInsightsResourceId>/subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/Markdig.WebApp</ApplicationInsightsResourceId>
<ApplicationInsightsAnnotationResourceId>/subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/Markdig.WebApp</ApplicationInsightsAnnotationResourceId>
</PropertyGroup>
<ItemGroup>
@@ -21,16 +22,20 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="1.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.0.2" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -13,6 +13,7 @@ namespace Markdig.WebApp
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseApplicationInsights()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -46,10 +46,6 @@ namespace Markdig.WebApp
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseMvc();
}
}

View File

@@ -1,4 +1,4 @@
{
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
@@ -6,5 +6,8 @@
"System": "Information",
"Microsoft": "Information"
}
},
"ApplicationInsights": {
"InstrumentationKey": "5d12f113-76b2-41fe-a35a-db454b104bf9"
}
}
}

View File

@@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using Markdig.Helpers;
@@ -89,13 +90,41 @@ namespace Markdig.Extensions.AutoIdentifiers
Heading = headingBlock,
CreateLinkInline = CreateLinkInlineForHeading
};
processor.Document.SetLinkReferenceDefinition(text, linkRef);
var doc = processor.Document;
var dictionary = doc.GetData(this) as Dictionary<string, HeadingLinkReferenceDefinition>;
if (dictionary == null)
{
dictionary = new Dictionary<string, HeadingLinkReferenceDefinition>();
doc.SetData(this, dictionary);
doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
}
dictionary[text] = linkRef;
}
// Then we register after inline have been processed to actually generate the proper #id
headingBlock.ProcessInlinesEnd += HeadingBlock_ProcessInlinesEnd;
}
private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline inline)
{
var doc = processor.Document;
doc.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin;
var dictionary = (Dictionary<string, HeadingLinkReferenceDefinition>)doc.GetData(this);
foreach (var keyPair in dictionary)
{
// Here we make sure that auto-identifiers will not override an existing link definition
// defined in the document
// If it is the case, we skip the auto identifier for the Heading
if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef))
{
doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value);
}
}
// Once we are done, we don't need to keep the intermediate dictionary arround
doc.RemoveData(this);
}
/// <summary>
/// Callback when there is a reference to found to a heading.
/// Note that reference are only working if they are declared after.
@@ -144,13 +173,15 @@ namespace Markdig.Extensions.AutoIdentifiers
var headingText = headingWriter.ToString();
headingWriter.GetStringBuilder().Length = 0;
// TODO: Should we have a struct with more configure optionss for LinkHelper.Urilize?
headingText = LinkHelper.Urilize(headingText,
(options & AutoIdentifierOptions.AllowOnlyAscii) != 0,
(options & AutoIdentifierOptions.KeepOpeningDigits) != 0,
(options & AutoIdentifierOptions.DiscardDots) != 0);
// Urilize the link
headingText = (options & AutoIdentifierOptions.GitHub) != 0
? LinkHelper.UrilizeAsGfm(headingText)
: LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
// If the heading is empty, use the word "section" instead
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
// Add a trailing -1, -2, -3...etc. in case of collision
int index = 0;
var headingId = baseHeadingId;
var headingBuffer = StringBuilderCache.Local();

View File

@@ -21,11 +21,6 @@ namespace Markdig.Extensions.AutoIdentifiers
/// </summary>
Default = AutoLink | AllowOnlyAscii,
/// <summary>
/// Renders auto identifiers like GitHub.
/// </summary>
GitHub = Default | KeepOpeningDigits | DiscardDots,
/// <summary>
/// Allows to link to a header by using the same text as the header for the link label. Default is <c>true</c>
/// </summary>
@@ -37,13 +32,8 @@ namespace Markdig.Extensions.AutoIdentifiers
AllowOnlyAscii = 2,
/// <summary>
/// Allows to keep digits starting a heading (by default, it keeps only characters starting from the first letter)
/// Renders auto identifiers like GitHub.
/// </summary>
KeepOpeningDigits = 4,
/// <summary>
/// Discard dots when computing an identifier.
/// </summary>
DiscardDots = 8
GitHub = 4,
}
}

View File

@@ -1,9 +1,10 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
using Markdig.Syntax.Inlines;
using Markdig.Renderers.Normalize;
using Markdig.Renderers.Normalize.Inlines;
namespace Markdig.Extensions.AutoLinks
{
@@ -24,6 +25,11 @@ namespace Markdig.Extensions.AutoLinks
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
var normalizeRenderer = renderer as NormalizeRenderer;
if (normalizeRenderer != null && !normalizeRenderer.ObjectRenderers.Contains<NormalizeAutoLinkRenderer>())
{
normalizeRenderer.ObjectRenderers.InsertBefore<LinkInlineRenderer>(new NormalizeAutoLinkRenderer());
}
}
}
}

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.
@@ -30,11 +30,18 @@ namespace Markdig.Extensions.AutoLinks
};
}
private static bool IsValidPreviousCharacter(char c)
{
// All such recognized autolinks can only come at the beginning of a line, after whitespace, or any of the delimiting characters *, _, ~, and (.
return c.IsWhiteSpaceOrZero() || c == '*' || c == '_' || c == '~' || c == '(';
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// Previous char must be a whitespace or a punctuation
var previousChar = slice.PeekCharExtra(-1);
if (!previousChar.IsAsciiPunctuation() && !previousChar.IsWhiteSpaceOrZero())
if (!IsValidPreviousCharacter(previousChar))
{
return false;
}
@@ -72,7 +79,7 @@ namespace Markdig.Extensions.AutoLinks
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1) || previousChar == '/') // We won't match http:/www. or /www.xxx
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
{
return false;
}
@@ -81,7 +88,7 @@ namespace Markdig.Extensions.AutoLinks
// Parse URL
string link;
if (!LinkHelper.TryParseUrl(ref slice, out link))
if (!LinkHelper.TryParseUrl(ref slice, out link, true))
{
return false;
}
@@ -151,7 +158,11 @@ namespace Markdig.Extensions.AutoLinks
Column = column,
Url = c == 'w' ? "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()
@@ -159,7 +170,7 @@ namespace Markdig.Extensions.AutoLinks
Span = inline.Span,
Line = line,
Column = column,
Content = new StringSlice(slice.Text, startPosition, startPosition + link.Length - 1),
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
IsClosed = true
});
processor.Inline = inline;

View File

@@ -0,0 +1,29 @@
using Markdig.Renderers;
using Markdig.Renderers.Normalize;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.AutoLinks
{
public class NormalizeAutoLinkRenderer : NormalizeObjectRenderer<LinkInline>
{
public override bool Accept(RendererBase renderer, MarkdownObject obj)
{
if (base.Accept(renderer, obj))
{
var normalizeRenderer = renderer as NormalizeRenderer;
var link = obj as LinkInline;
return normalizeRenderer != null && link != null && !normalizeRenderer.Options.ExpandAutoLinks && link.IsAutoLink;
}
else
{
return false;
}
}
protected override void Write(NormalizeRenderer renderer, LinkInline obj)
{
renderer.Write(obj.Url);
}
}
}

View File

@@ -12,19 +12,19 @@ namespace Markdig.Extensions.Emoji
/// <seealso cref="Markdig.IMarkdownExtension" />
public class EmojiExtension : IMarkdownExtension
{
private readonly bool _enableSmiley;
public EmojiExtension(bool enableSmiley = true)
{
_enableSmiley = enableSmiley;
EnableSmiley = enableSmiley;
}
public bool EnableSmiley { get; set; }
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(EnableSmiley));
}
}

View File

@@ -1,8 +1,10 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Normalize.Inlines;
using Markdig.Renderers.Normalize;
namespace Markdig.Extensions.JiraLinks
{
@@ -30,7 +32,13 @@ namespace Markdig.Extensions.JiraLinks
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
// Nothing to setup, JiraLinks used a normal LinkInlineRenderer
// No HTML renderer required, since JiraLink type derives from InlineLink (which already has an HTML renderer)
var normalizeRenderer = renderer as NormalizeRenderer;
if (normalizeRenderer != null && !normalizeRenderer.ObjectRenderers.Contains<NormalizeJiraLinksRenderer>())
{
normalizeRenderer.ObjectRenderers.InsertBefore<LinkInlineRenderer>(new NormalizeJiraLinksRenderer());
}
}
}

View File

@@ -0,0 +1,18 @@
using Markdig.Renderers.Normalize;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Markdig.Extensions.JiraLinks
{
public class NormalizeJiraLinksRenderer : NormalizeObjectRenderer<JiraLink>
{
protected override void Write(NormalizeRenderer renderer, JiraLink obj)
{
renderer.Write(obj.ProjectKey);
renderer.Write("-");
renderer.Write(obj.Issue);
}
}
}

View File

@@ -1,8 +1,10 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Renderers.Html.Inlines;
@@ -47,88 +49,170 @@ namespace Markdig.Extensions.MediaLinks
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
{
if (linkInline.IsImage && linkInline.Url != null)
if (!linkInline.IsImage || linkInline.Url == null)
{
Uri uri;
// Only process absolute Uri
if (Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri) && uri.IsAbsoluteUri)
return false;
}
Uri uri;
// Only process absolute Uri
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri) || !uri.IsAbsoluteUri)
{
return false;
}
if (TryRenderIframeFromKnownProviders(uri, renderer, linkInline))
{
return true;
}
if (TryGuessAudioVideoFile(uri, renderer, linkInline))
{
return true;
}
return false;
}
private static HtmlAttributes GetHtmlAttributes(LinkInline linkInline)
{
var htmlAttributes = new HtmlAttributes();
var fromAttributes = linkInline.TryGetAttributes();
if (fromAttributes != null)
{
fromAttributes.CopyTo(htmlAttributes, false, false);
}
return htmlAttributes;
}
private bool TryGuessAudioVideoFile(Uri uri, 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
string mimeType;
var lastDot = path.LastIndexOf('.');
if (lastDot >= 0 &&
Options.ExtensionToMimeType.TryGetValue(path.Substring(lastDot), out mimeType))
{
var htmlAttributes = GetHtmlAttributes(linkInline);
var isAudio = mimeType.StartsWith("audio");
var tagType = isAudio ? "audio" : "video";
renderer.Write($"<{tagType}");
htmlAttributes.AddPropertyIfNotExist("width", Options.Width);
if (!isAudio)
{
var htmlAttributes = new HtmlAttributes();
var fromAttributes = linkInline.TryGetAttributes();
if (fromAttributes != null)
{
fromAttributes.CopyTo(htmlAttributes, false, false);
}
// TODO: this code is not pluggable, so for now, we handle only the following web providers:
// - youtube
// - vimeo
var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
string iFrameUrl = null;
if (uri.Host.StartsWith("www.youtube.com", StringComparison.OrdinalIgnoreCase))
{
var query = SplitQuery(uri);
if (query.Length > 0 && query[0].StartsWith("v="))
{
iFrameUrl = $"https://www.youtube.com/embed/{query[0].Substring(2)}";
}
}
else if (uri.Host.StartsWith("vimeo.com", StringComparison.OrdinalIgnoreCase))
{
var items = path.Split('/');
if (items.Length > 0)
{
iFrameUrl = $"https://player.vimeo.com/video/{items[items.Length - 1]}";
}
}
if (iFrameUrl != null)
{
renderer.Write($"<iframe src=\"{iFrameUrl}\"");
htmlAttributes.AddPropertyIfNotExist("width", Options.Width);
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
htmlAttributes.AddPropertyIfNotExist("frameborder", "0");
htmlAttributes.AddPropertyIfNotExist("allowfullscreen", null);
renderer.WriteAttributes(htmlAttributes);
renderer.Write("></iframe>");
return true;
}
else
{
// Otherwise try to detect if we have an audio/video from the file extension
string mimeType;
var lastDot = path.LastIndexOf('.');
if (lastDot >= 0 &&
Options.ExtensionToMimeType.TryGetValue(path.Substring(lastDot), out mimeType))
{
var isAudio = mimeType.StartsWith("audio");
var tagType = isAudio ? "audio" : "video";
renderer.Write($"<{tagType}");
htmlAttributes.AddPropertyIfNotExist("width", Options.Width);
if (!isAudio)
{
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
}
htmlAttributes.AddPropertyIfNotExist("controls", null);
renderer.WriteAttributes(htmlAttributes);
renderer.Write($"><source type=\"{mimeType}\" src=\"{linkInline.Url}\"></source></{tagType}>");
return true;
}
}
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
}
htmlAttributes.AddPropertyIfNotExist("controls", null);
renderer.WriteAttributes(htmlAttributes);
renderer.Write($"><source type=\"{mimeType}\" src=\"{linkInline.Url}\"></source></{tagType}>");
return true;
}
return false;
}
#region Known providers
private class KnownProvider
{
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 = "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
if (foundProvider == null)
{
return false;
}
var htmlAttributes = GetHtmlAttributes(linkInline);
renderer.Write($"<iframe src=\"{foundProvider.Result}\"");
htmlAttributes.AddPropertyIfNotExist("width", Options.Width);
htmlAttributes.AddPropertyIfNotExist("height", Options.Height);
htmlAttributes.AddPropertyIfNotExist("frameborder", "0");
if (foundProvider.AllowFullScreen)
{
htmlAttributes.AddPropertyIfNotExist("allowfullscreen", null);
}
renderer.WriteAttributes(htmlAttributes);
renderer.Write("></iframe>");
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)
{
var query = SplitQuery(uri);
return query.Length > 0 && query[0].StartsWith("v=")
? $"https://www.youtube.com/embed/{query[0].Substring(2)}"
: null;
}
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

@@ -68,6 +68,7 @@ namespace Markdig.Extensions.MediaLinks
{".wma", "audio/x-ms-wma"},
{".wax", "audio/x-ms-wax"},
{".mid", "audio/midi"},
{".mp3", "audio/mpeg"},
{".mpga", "audio/mpeg"},
{".mp4a", "audio/mp4"},
{".ecelp4800", "audio/vnd.nuera.ecelp4800"},
@@ -88,4 +89,4 @@ namespace Markdig.Extensions.MediaLinks
public Dictionary<string, string> ExtensionToMimeType { get; }
}
}
}

View File

@@ -0,0 +1,14 @@
using Markdig.Renderers.Normalize;
namespace Markdig.Extensions.TaskLists
{
public class NormalizeTaskListRenderer : NormalizeObjectRenderer<TaskList>
{
protected override void Write(NormalizeRenderer renderer, TaskList obj)
{
renderer.Write("[");
renderer.Write(obj.Checked ? "X" : " ");
renderer.Write("]");
}
}
}

View File

@@ -1,9 +1,10 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Normalize;
namespace Markdig.Extensions.TaskLists
{
@@ -28,6 +29,12 @@ namespace Markdig.Extensions.TaskLists
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlTaskListRenderer>();
}
var normalizeRenderer = renderer as NormalizeRenderer;
if (normalizeRenderer != null)
{
normalizeRenderer.ObjectRenderers.AddIfNotAlready<NormalizeTaskListRenderer>();
}
}
}
}

View File

@@ -67,17 +67,55 @@ namespace Markdig.Extensions.Yaml
// this is a YAML front matter blcok
if (count == 3 && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
{
// Create a front matter block
var block = this.CreateFrontMatterBlock(processor);
block.Column = processor.Column;
block.Span.Start = 0;
block.Span.End = line.Start;
bool hasFullYamlFrontMatter = false;
// We make sure that there is a closing frontmatter somewhere in the document
// so here we work on the full document instead of just the line
var fullLine = new StringSlice(line.Text, line.Start, line.Text.Length - 1);
c = fullLine.CurrentChar;
while (c != '\0')
{
c = fullLine.NextChar();
if (c == '\n' || c == '\r')
{
var nc = fullLine.PeekChar();
if (c == '\r' && nc == '\n')
{
c = fullLine.NextChar();
}
nc = fullLine.PeekChar();
if (nc == '-')
{
if (fullLine.NextChar() == '-' && fullLine.NextChar() == '-' && fullLine.NextChar() == '-' && (fullLine.NextChar() == '\0' || fullLine.SkipSpacesToEndOfLineOrEndOfDocument()))
{
hasFullYamlFrontMatter = true;
break;
}
}
else if (nc == '.')
{
if (fullLine.NextChar() == '.' && fullLine.NextChar() == '.' && fullLine.NextChar() == '.' && (fullLine.NextChar() == '\0' || fullLine.SkipSpacesToEndOfLineOrEndOfDocument()))
{
hasFullYamlFrontMatter = true;
break;
}
}
}
}
// Store the number of matched string into the context
processor.NewBlocks.Push(block);
if (hasFullYamlFrontMatter)
{
// Create a front matter block
var block = this.CreateFrontMatterBlock(processor);
block.Column = processor.Column;
block.Span.Start = 0;
block.Span.End = line.Start;
// Discard the current line as it is already parsed
return BlockState.ContinueDiscard;
// Store the number of matched string into the context
processor.NewBlocks.Push(block);
// Discard the current line as it is already parsed
return BlockState.ContinueDiscard;
}
}
return BlockState.None;

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.
@@ -472,10 +472,10 @@ namespace Markdig.Helpers
}
else if (c == '&')
{
string namedEntity;
int entityNameStart;
int entityNameLength;
int numericEntity;
var match = ScanEntity(text, searchPos, text.Length - searchPos, out namedEntity,
out numericEntity);
var match = ScanEntity(new StringSlice(text, searchPos, text.Length - 1), out numericEntity, out entityNameStart, out entityNameLength);
if (match == 0)
{
searchPos++;
@@ -484,9 +484,10 @@ namespace Markdig.Helpers
{
searchPos += match;
if (namedEntity != null)
if (entityNameLength > 0)
{
var decoded = EntityHelper.DecodeEntity(namedEntity);
var namedEntity = new StringSlice(text, entityNameStart, entityNameStart + entityNameLength - 1);
var decoded = EntityHelper.DecodeEntity(namedEntity.ToString());
if (decoded != null)
{
sb.Append(text, lastPos, searchPos - match - lastPos);
@@ -533,7 +534,7 @@ namespace Markdig.Helpers
/// Scans an entity.
/// Returns number of chars matched.
/// </summary>
public static int ScanEntity(string s, int pos, int length, out string namedEntity, out int numericEntity)
public static int ScanEntity<T>(T slice, out int numericEntity, out int namedEntityStart, out int namedEntityLength) where T : ICharIterator
{
// Credits: code from CommonMark.NET
// Copyright (c) 2014, Kārlis Gaņģis All rights reserved.
@@ -545,29 +546,29 @@ namespace Markdig.Helpers
.? { return 0; }
*/
var lastPos = pos + length;
namedEntity = null;
numericEntity = 0;
namedEntityStart = 0;
namedEntityLength = 0;
if (pos + 3 >= lastPos)
return 0;
if (s[pos] != '&')
return 0;
char c;
int i;
int counter = 0;
if (s[pos + 1] == '#')
if (slice.CurrentChar != '&' || slice.PeekChar(3) == '\0')
{
c = s[pos + 2];
return 0;
}
var start = slice.Start;
char c = slice.NextChar();
int counter = 0;
if (c == '#')
{
c = slice.PeekChar();
if (c == 'x' || c == 'X')
{
c = slice.NextChar(); // skip #
// expect 1-8 hex digits starting from pos+3
for (i = pos + 3; i < lastPos; i++)
while (c != '\0')
{
c = s[i];
c = slice.NextChar();
if (c >= '0' && c <= '9')
{
if (++counter == 9) return 0;
@@ -588,7 +589,7 @@ namespace Markdig.Helpers
}
if (c == ';')
return counter == 0 ? 0 : i - pos + 1;
return counter == 0 ? 0 : slice.Start - start + 1;
return 0;
}
@@ -596,9 +597,10 @@ namespace Markdig.Helpers
else
{
// expect 1-8 digits starting from pos+2
for (i = pos + 2; i < lastPos; i++)
while (c != '\0')
{
c = s[i];
c = slice.NextChar();
if (c >= '0' && c <= '9')
{
if (++counter == 9) return 0;
@@ -607,7 +609,7 @@ namespace Markdig.Helpers
}
if (c == ';')
return counter == 0 ? 0 : i - pos + 1;
return counter == 0 ? 0 : slice.Start - start + 1;
return 0;
}
@@ -616,25 +618,26 @@ namespace Markdig.Helpers
else
{
// expect a letter and 1-31 letters or digits
c = s[pos + 1];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')))
return 0;
for (i = pos + 2; i < lastPos; i++)
namedEntityStart = slice.Start;
namedEntityLength++;
while (c != '\0')
{
c = s[i];
c = slice.NextChar();
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
{
if (++counter == 32)
return 0;
namedEntityLength++;
continue;
}
if (c == ';')
{
namedEntity = s.Substring(pos + 1, counter + 1);
return counter == 0 ? 0 : i - pos + 1;
return counter == 0 ? 0 : slice.Start - start + 1;
}
return 0;

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.
@@ -34,8 +34,9 @@ namespace Markdig.Helpers
/// <summary>
/// Peeks at the next character, without incrementing the <see cref="Start"/> position.
/// </summary>
/// <param name="offset"></param>
/// <returns>The next character. `\0` is end of the iteration.</returns>
char PeekChar();
char PeekChar(int offset = 1);
/// <summary>
/// Gets a value indicating whether this instance is empty.

View File

@@ -4,6 +4,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
using Markdig.Parsers.Inlines;
using Markdig.Syntax;
namespace Markdig.Helpers
@@ -18,7 +19,7 @@ namespace Markdig.Helpers
return TryParseAutolink(ref text, out link, out isEmail);
}
public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false, bool discardDots = false)
public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
{
var headingBuffer = StringBuilderCache.Local();
bool hasLetter = keepOpeningDigits && headingText.Length > 0 && char.IsLetterOrDigit(headingText[0]);
@@ -47,7 +48,7 @@ namespace Markdig.Helpers
}
else if (hasLetter)
{
if (IsReservedPunctuation(c, discardDots))
if (IsReservedPunctuation(c))
{
if (previousIsSpace)
{
@@ -67,7 +68,7 @@ namespace Markdig.Helpers
else if (!previousIsSpace && c.IsWhitespace())
{
var pc = headingBuffer[headingBuffer.Length - 1];
if (!IsReservedPunctuation(pc, discardDots))
if (!IsReservedPunctuation(pc))
{
headingBuffer.Append('-');
}
@@ -81,7 +82,7 @@ namespace Markdig.Helpers
while (headingBuffer.Length > 0)
{
var c = headingBuffer[headingBuffer.Length - 1];
if (IsReservedPunctuation(c, false))
if (IsReservedPunctuation(c))
{
headingBuffer.Length--;
}
@@ -96,10 +97,27 @@ namespace Markdig.Helpers
return text;
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
private static bool IsReservedPunctuation(char c, bool discardDots)
public static string UrilizeAsGfm(string headingText)
{
return c == '_' || c == '-' || (!discardDots && c == '.');
// Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
var headingBuffer = StringBuilderCache.Local();
for (int i = 0; i < headingText.Length; i++)
{
var c = char.ToLowerInvariant(headingText[i]);
if (char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_')
{
headingBuffer.Append(c == ' ' ? '-' : c);
}
}
var result = headingBuffer.ToString();
headingBuffer.Length = 0;
return result;
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
private static bool IsReservedPunctuation(char c)
{
return c == '_' || c == '-' || c == '.';
}
public static bool TryParseAutolink(ref StringSlice text, out string link, out bool isEmail)
@@ -496,7 +514,7 @@ namespace Markdig.Helpers
return TryParseUrl(ref text, out link);
}
public static bool TryParseUrl<T>(ref T text, out string link) where T : ICharIterator
public static bool TryParseUrl<T>(ref T text, out string link, bool isAutoLink = false) where T : ICharIterator
{
bool isValid = false;
var buffer = StringBuilderCache.Local();
@@ -593,16 +611,30 @@ namespace Markdig.Helpers
hasEscape = false;
if (IsEndOfUri(c))
if (IsEndOfUri(c, isAutoLink))
{
isValid = true;
break;
}
if (c == '.' && IsEndOfUri(text.PeekChar()))
if (isAutoLink)
{
isValid = true;
break;
if (c == '&')
{
int entityNameStart;
int entityNameLength;
int entityValue;
if (HtmlHelper.ScanEntity(text, out entityValue, out entityNameStart, out entityNameLength) > 0)
{
isValid = true;
break;
}
}
if (IsTrailingUrlStopCharacter(c) && IsEndOfUri(text.PeekChar(), true))
{
isValid = true;
break;
}
}
buffer.Append(c);
@@ -621,9 +653,17 @@ namespace Markdig.Helpers
return isValid;
}
private static bool IsEndOfUri(char c)
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
private static bool IsTrailingUrlStopCharacter(char c)
{
return c == '\0' || c.IsSpaceOrTab() || c.IsControl(); // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
// Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink, though they may be included in the interior of the link:
return c == '?' || c == '!' || c == '.' || c == ',' || c == ':' || c == '*' || c == '*' || c == '_' || c == '~';
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
private static bool IsEndOfUri(char c, bool isAutoLink)
{
return c == '\0' || c.IsSpaceOrTab() || c.IsControl() || (isAutoLink && c == '<'); // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
}
public static bool TryParseLinkReferenceDefinition<T>(T text, out string label, out string url,

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.
@@ -197,14 +197,14 @@ namespace Markdig.Helpers
/// <seealso cref="ICharIterator" />
public struct Iterator : ICharIterator
{
private readonly StringLineGroup lines;
private int offset;
private readonly StringLineGroup _lines;
private int _offset;
public Iterator(StringLineGroup lines)
{
this.lines = lines;
this._lines = lines;
Start = -1;
offset = -1;
_offset = -1;
SliceIndex = 0;
CurrentChar = '\0';
End = -2;
@@ -228,45 +228,47 @@ namespace Markdig.Helpers
public char NextChar()
{
Start++;
offset++;
_offset++;
if (Start <= End)
{
var slice = (StringSlice)lines.Lines[SliceIndex];
if (offset < slice.Length)
var slice = (StringSlice)_lines.Lines[SliceIndex];
if (_offset < slice.Length)
{
CurrentChar = slice[slice.Start + offset];
CurrentChar = slice[slice.Start + _offset];
}
else
{
CurrentChar = '\n';
SliceIndex++;
offset = -1;
_offset = -1;
}
}
else
{
CurrentChar = '\0';
Start = End + 1;
SliceIndex = lines.Count;
offset--;
SliceIndex = _lines.Count;
_offset--;
}
return CurrentChar;
}
public char PeekChar()
public char PeekChar(int offset = 1)
{
if (Start + 1 > End)
if (offset < 0) throw new ArgumentOutOfRangeException("Negative offset are not supported for StringLineGroup", nameof(offset));
if (Start + offset > End)
{
return '\0';
}
var slice = (StringSlice)lines.Lines[SliceIndex];
if (offset + 1 >= slice.Length)
var slice = (StringSlice)_lines.Lines[SliceIndex];
if (_offset + offset >= slice.Length)
{
return '\n';
}
return slice[slice.Start + offset + 1];
return slice[slice.Start + _offset + offset];
}
public bool TrimStart()

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.
@@ -106,23 +106,12 @@ 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)
public char PeekChar(int offset = 1)
{
var index = Start + offset;
return index >= Start && index <= End ? Text[index] : (char) 0;
}
/// <summary>
/// Peeks the character immediately after the current <see cref="Start"/> position
/// or returns `\0` if after the <see cref="End"/> position.
/// </summary>
/// <returns>The next character, returns `\0` if none.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public char PeekChar()
{
return PeekChar(1);
}
/// <summary>
/// Peeks a character at the specified offset from the current beginning of the string, without taking into account <see cref="Start"/> and <see cref="End"/>
/// </summary>
@@ -179,6 +168,28 @@ namespace Markdig.Helpers
return i == text.Length;
}
/// <summary>
/// Expect spaces until a end of line. Return <c>false</c> otherwise.
/// </summary>
/// <returns><c>true</c> if whitespaces where matched until a end of line</returns>
public bool SkipSpacesToEndOfLineOrEndOfDocument()
{
for (int i = Start; i <= End; i++)
{
var c = Text[i];
if (c.IsWhitespace())
{
if (c == '\0' || c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n'))
{
return true;
}
continue;
}
return false;
}
return true;
}
/// <summary>
/// Matches the specified text using lowercase comparison.
/// </summary>

View File

@@ -5,34 +5,19 @@
<Copyright>Alexandre Mutel</Copyright>
<AssemblyTitle>Markdig</AssemblyTitle>
<NeutralLanguage>en-US</NeutralLanguage>
<VersionPrefix>0.14.2</VersionPrefix>
<VersionPrefix>0.14.9</VersionPrefix>
<Authors>Alexandre Mutel</Authors>
<TargetFrameworks>net35;net40;portable40-net40+sl5+win8+wp8+wpa81;netstandard1.1;uap10.0</TargetFrameworks>
<AssemblyName>Markdig</AssemblyName>
<PackageId>Markdig</PackageId>
<PackageId Condition="'$(SignAssembly)' == 'true'">Markdig.Signed</PackageId>
<PackageTags>Markdown CommonMark md html md2html</PackageTags>
<PackageReleaseNotes>
&gt; 0.14.2
- Fix issue with emphasis preceded/followed by an HTML entity (#157)
- Add support for link reference definitions for Normalize renderer (#155)
- Add option to disable smiley parsing in EmojiAndSmiley extension
&gt; 0.14.1
- Fix crash in Markdown.Normalize to handle HtmlBlock correctly
- Add better handling of bullet character for lists in Markdown.Normalize
&gt; 0.14.0
- Add Markdown.ToPlainText, Add option HtmlRenderer.EnableHtmlForBlock.
- Add Markdown.Normalize, to allow to normalize a markdown document. Add NormalizeRenderer, to render a MarkdownDocument back to markdown.
-
&gt; 0.13.4
- Add support for single table header row without a table body rows (#141)
- ADd support for `nomnoml` diagrams
</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/lunet-io/markdig/blob/master/changelog.md</PackageReleaseNotes>
<PackageIconUrl>https://raw.githubusercontent.com/lunet-io/markdig/master/img/markdig.png</PackageIconUrl>
<PackageProjectUrl>https://github.com/lunet-io/markdig</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/lunet-io/markdig/blob/master/license.txt</PackageLicenseUrl>
<NetStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard1.1' ">1.6.0</NetStandardImplicitPackageVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
@@ -65,21 +50,21 @@
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.1' ">
<DefineConstants>$(DefineConstants);NETSTANDARD_11;SUPPORT_UNSAFE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'uap10.0' ">
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition="'$(TargetPlatformVersion)' == ''">10.0.10240.0</TargetPlatformVersion>
<TargetPlatformMinVersion Condition="'$(TargetPlatformMinVersion)' == ''">10.0.10240.0</TargetPlatformMinVersion>
<TargetPlatformMinVersion Condition="'$(TargetPlatformMinVersion)' == ''">10.0.10240.0</TargetPlatformMinVersion>
<DefineConstants>$(DefineConstants);NETSTANDARD_11;SUPPORT_UNSAFE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'portable40-net40+sl5+win8+wp8+wpa81'">
<TargetFrameworkIdentifier>.NETPortable</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile>Profile328</TargetFrameworkProfile>
<LanguageTargets>$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets</LanguageTargets>
</PropertyGroup>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

View File

@@ -1,6 +1,8 @@
// 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.Text;
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -25,17 +27,18 @@ namespace Markdig.Parsers.Inlines
public static bool TryParse(ref StringSlice slice, out string literal, out int match)
{
literal = null;
string entityName;
int entityNameStart;
int entityNameLength;
int entityValue;
match = HtmlHelper.ScanEntity(slice.Text, slice.Start, slice.Length, out entityName, out entityValue);
match = HtmlHelper.ScanEntity(slice, out entityValue, out entityNameStart, out entityNameLength);
if (match == 0)
{
return false;
}
if (entityName != null)
if (entityNameLength > 0)
{
literal = EntityHelper.DecodeEntity(entityName);
literal = EntityHelper.DecodeEntity(new StringSlice(slice.Text, entityNameStart, entityNameStart + entityNameLength - 1).ToString());
}
else if (entityValue >= 0)
{

View File

@@ -86,7 +86,7 @@ namespace Markdig.Parsers
// interpretations of a line, the thematic break takes precedence
BlockState result;
var thematicParser = ThematicBreakParser.Default;
if (thematicParser.HasOpeningCharacter(processor.CurrentChar))
if (!(processor.LastBlock is FencedCodeBlock) && thematicParser.HasOpeningCharacter(processor.CurrentChar))
{
result = thematicParser.TryOpen(processor);
if (result.IsBreak())

View File

@@ -26,6 +26,7 @@ namespace Markdig.Renderers.Html.Inlines
renderer.Write("mailto:");
}
renderer.WriteEscapeUrl(obj.Url);
renderer.Write('"');
renderer.WriteAttributes(obj);
if (!obj.IsEmail && AutoRelNoFollow)
@@ -33,7 +34,7 @@ namespace Markdig.Renderers.Html.Inlines
renderer.Write(" rel=\"nofollow\"");
}
renderer.Write("\">");
renderer.Write(">");
}
renderer.WriteEscape(obj.Url);

View File

@@ -17,6 +17,7 @@ namespace Markdig.Renderers.Normalize
EmptyLineAfterCodeBlock = true;
EmptyLineAfterHeading = true;
EmptyLineAfterThematicBreak = true;
ExpandAutoLinks = true;
ListItemCharacter = null;
}
@@ -44,5 +45,10 @@ namespace Markdig.Renderers.Normalize
/// The bullet character used for list items. Default is <c>null</c> leaving the original bullet character as-is.
/// </summary>
public char? ListItemCharacter { get; set; }
/// <summary>
/// Expands AutoLinks to the normal inline representation. Default is <c>true</c>
/// </summary>
public bool ExpandAutoLinks { get; set; }
}
}

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;
@@ -192,17 +192,16 @@ namespace Markdig.Syntax.Inlines
/// <returns><c>true</c> if this instance contains a parent of the specified type; <c>false</c> otherwise</returns>
public bool ContainsParentOfType<T>() where T : Inline
{
var delimiter = this as T;
if (delimiter != null)
var inline = this;
while (inline != null)
{
return true;
var delimiter = inline as T;
if (delimiter != null)
{
return true;
}
inline = inline.Parent;
}
if (Parent != null)
{
return Parent.ContainsParentOfType<T>();
}
return false;
}
@@ -213,19 +212,15 @@ namespace Markdig.Syntax.Inlines
/// <returns>An enumeration on the parents of the specified type</returns>
public IEnumerable<T> FindParentOfType<T>() where T : Inline
{
var parent = Parent;
var delimiter = this as T;
if (delimiter != null)
var inline = this;
while (inline != null)
{
yield return delimiter;
}
if (parent != null)
{
foreach (var previous in parent.FindParentOfType<T>())
var delimiter = inline as T;
if (delimiter != null)
{
yield return previous;
yield return delimiter;
}
inline = inline.Parent;
}
}

View File

@@ -69,6 +69,11 @@ namespace Markdig.Syntax.Inlines
/// </summary>
public bool IsShortcut { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized.
/// </summary>
public bool IsAutoLink { get; set; }
/// <summary>
/// Gets or sets the reference this link is attached to. May be null.
/// </summary>

View File

@@ -1,11 +1,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
VisualStudioVersion = 15.0.27004.2005
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{061866E2-005C-4D13-A338-EA464BBEC60F}"
ProjectSection(SolutionItems) = preProject
..\appveyor.yml = ..\appveyor.yml
..\changelog.md = ..\changelog.md
..\license.txt = ..\license.txt
..\readme.md = ..\readme.md
EndProjectSection