feat: initial commit

This commit is contained in:
Dylan R. E. Moonfire 2021-09-06 23:56:25 -05:00
commit 9fe90f820c
61 changed files with 16325 additions and 0 deletions

122
.editorconfig Normal file
View File

@ -0,0 +1,122 @@
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
charset=utf-8
end_of_line = lf
insert_final_newline=true
indent_style=space
indent_size=4
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast=false
csharp_style_var_elsewhere=false:hint
csharp_style_var_for_built_in_types=false:hint
csharp_style_var_when_type_is_apparent=true:hint
csharp_preserve_single_line_statements=false
csharp_preserve_single_line_blocks=true
dotnet_style_predefined_type_for_locals_parameters_members=true:hint
dotnet_style_predefined_type_for_member_access=true:hint
dotnet_style_qualification_for_event=true:hint
dotnet_style_qualification_for_field=true:hint
dotnet_style_qualification_for_method=true:hint
dotnet_style_qualification_for_property=true:hint
dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint
# ReSharper properties
resharper_alignment_tab_fill_style=optimal_fill
resharper_apply_on_completion=true
resharper_blank_lines_after_control_transfer_statements=1
resharper_blank_lines_around_single_line_auto_property=1
resharper_blank_lines_around_single_line_property=1
resharper_blank_lines_before_single_line_comment=1
resharper_blank_lines_between_using_groups=1
resharper_braces_for_for=required
resharper_braces_for_foreach=required
resharper_braces_for_ifelse=required
resharper_braces_for_while=required
resharper_can_use_global_alias=false
resharper_csharp_blank_lines_around_single_line_field=1
resharper_csharp_blank_lines_around_single_line_invocable=1
resharper_csharp_indent_style=tab
resharper_csharp_insert_final_newline=true
resharper_csharp_keep_blank_lines_in_code=1
resharper_csharp_keep_blank_lines_in_declarations=1
resharper_csharp_new_line_before_while=true
resharper_csharp_use_indent_from_vs=false
resharper_csharp_wrap_arguments_style=chop_if_long
resharper_csharp_wrap_extends_list_style=chop_if_long
resharper_csharp_wrap_parameters_style=chop_if_long
resharper_css_insert_final_newline=false
resharper_enforce_line_ending_style=true
resharper_html_insert_final_newline=false
resharper_indent_nested_fixed_stmt=true
resharper_js_indent_style=tab
resharper_js_insert_final_newline=true
resharper_js_keep_blank_lines_in_code=1
resharper_js_stick_comment=false
resharper_js_use_indent_from_vs=false
resharper_js_wrap_before_binary_opsign=true
resharper_js_wrap_chained_method_calls=chop_if_long
resharper_keep_blank_lines_between_declarations=1
resharper_min_blank_lines_after_imports=1
resharper_place_attribute_on_same_line=False
resharper_place_constructor_initializer_on_same_line=false
resharper_place_type_constraints_on_same_line=false
resharper_protobuf_insert_final_newline=false
resharper_qualified_using_at_nested_scope=true
resharper_resx_insert_final_newline=false
resharper_space_within_single_line_array_initializer_braces=true
resharper_use_indents_from_main_language_in_file=false
resharper_vb_insert_final_newline=false
resharper_wrap_after_declaration_lpar=true
resharper_wrap_after_invocation_lpar=true
resharper_wrap_before_extends_colon=true
resharper_wrap_before_first_type_parameter_constraint=true
resharper_wrap_before_type_parameter_langle=true
resharper_xmldoc_indent_child_elements=ZeroIndent
resharper_xmldoc_indent_text=ZeroIndent
resharper_xmldoc_insert_final_newline=false
resharper_xml_insert_final_newline=false
# ReSharper inspection severities
resharper_check_namespace_highlighting=none
resharper_convert_to_auto_property_highlighting=none
resharper_localizable_element_highlighting=none
resharper_redundant_comma_in_attribute_list_highlighting=none
resharper_redundant_comma_in_enum_declaration_highlighting=none
resharper_redundant_comma_in_initializer_highlighting=none
resharper_string_compare_to_is_culture_specific_highlighting=none
resharper_string_index_of_is_culture_specific_1_highlighting=none
resharper_use_null_propagation_highlighting=none
resharper_use_object_or_collection_initializer_highlighting=hint
resharper_use_string_interpolation_highlighting=hint
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style=space
indent_size=2
tab_width=2
[*.{cs,js,json,jsx,proto,resjson,ts,tsx}]
indent_style=space
indent_size=space
tab_width=4
[*.{asax,ascx,aspx,cshtml,css,htm,html,master,razor,skin,vb,xaml,xamlx,xoml}]
indent_style=space
indent_size=4
tab_width=4
[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
indent_style=space
indent_size=2
tab_width=2
[*.proto]
indent_style=space
indent_size=2
tab_width=2

1
.envrc Normal file
View File

@ -0,0 +1 @@
use asdf

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
launchSettings.json
*~
*.user
Directory.Build.props
obj/
[Bb]in/
.vs/
.vscode/
.idea/
_ReSharper.Caches/
node_modules/

51
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,51 @@
stages:
- build
default:
before_script:
- curl -sL https://deb.nodesource.com/setup_15.x | bash -
- apt-get install -y nodejs
build:
image: mcr.microsoft.com/dotnet/sdk:5.0
stage: build
script:
# Set up the environment.
- npx npm install --ci
- npx commitlint-gitlab-ci -x @commitlint/config-conventional
# Build and test everything.
- dotnet restore
- dotnet build
- 'dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"'
# Summarize the output for Gitlab CI reporting.
- dotnet new tool-manifest
- dotnet tool install dotnet-reportgenerator-globaltool
- dotnet tool run reportgenerator -reports:src/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
- grep "Line coverage" coverage/Summary.txt
# Perform the release.
- npx semantic-release
rules:
- if: '$CI_COMMIT_TITLE =~ /^chore\(release\)/'
when: never
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
when: never
- when: on_success
artifacts:
when: always
paths:
- ./**/*test-result.xml
- ./coverage/Cobertura.xml
- ./coverage/Summary.*
- ./**/*.nupkg
reports:
junit:
- ./**/*test-result.xml
cobertura:
- ./coverage/Cobertura.xml

4
.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

3
.tool-versions Normal file
View File

@ -0,0 +1,3 @@
dotnet-core 5.0.100
yarn 1.22.10
nodejs 15.0.1

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Moonfire Games
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,54 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{954BA984-D50E-4D1C-880F-EAE678EF6945}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown.Gemtext", "src\MfGames.Markdown.Gemtext\MfGames.Markdown.Gemtext.csproj", "{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown.Gemtext.Tests", "src\MfGames.Markdown.Gemtext.Tests\MfGames.Markdown.Gemtext.Tests.csproj", "{D2703B25-9AF7-49FF-93A4-CB124560F2A9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Debug|x64.ActiveCfg = Debug|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Debug|x64.Build.0 = Debug|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Debug|x86.ActiveCfg = Debug|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Debug|x86.Build.0 = Debug|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Release|Any CPU.Build.0 = Release|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Release|x64.ActiveCfg = Release|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Release|x64.Build.0 = Release|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Release|x86.ActiveCfg = Release|Any CPU
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA}.Release|x86.Build.0 = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Debug|x64.Build.0 = Debug|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Debug|x86.Build.0 = Debug|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|Any CPU.Build.0 = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x64.ActiveCfg = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x64.Build.0 = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x86.ActiveCfg = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA} = {954BA984-D50E-4D1C-880F-EAE678EF6945}
{D2703B25-9AF7-49FF-93A4-CB124560F2A9} = {954BA984-D50E-4D1C-880F-EAE678EF6945}
EndGlobalSection
EndGlobal

File diff suppressed because it is too large Load Diff

4
README.md Normal file
View File

@ -0,0 +1,4 @@
MfGames.Markdown.Gemtext CIL
============================
An extension for [Markdig](https://github.com/xoofx/markdig) that converts Markdown into Gemtext.

3
commitlint.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
};

12304
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "mfgames-markdown-gemtext-cil",
"version": "1.0.0",
"private": true,
"scripts": {
"prepare": "husky install"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@semantic-release/gitlab": "^6.2.2",
"@semantic-release/npm": "^7.1.3",
"commitlint-gitlab-ci": "^0.0.4",
"husky": "^7.0.2",
"semantic-release": "^17.4.7",
"semantic-release-dotnet": "^1.0.0",
"semantic-release-nuget": "^1.1.0"
}
}

20
release.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
branches: ["main"],
message: "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}",
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"semantic-release-dotnet",
[
"semantic-release-nuget",
{
packArguments: ["--include-symbols", "--include-source"],
pushFiles: ["src/*/bin/Debug/*.nupkg"],
},
],
"@semantic-release/changelog",
"@semantic-release/git",
"@semantic-release/gitlab",
],
};

View File

@ -0,0 +1,39 @@
using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class CodeInlineTests
{
[Fact]
public void Default()
{
string input = "Normal `code` quote.";
string expected = "Normal code quote.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void Normalized()
{
string input = "Normal `code` quote.";
string expected = "Normal `code` quote.";
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use(
new SetInlineFormatting
{
Code = InlineFormatting.Normalize,
})
.Build());
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,107 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class HeaderTests
{
[Fact]
public void BetweenParagraphs()
{
string input = string.Join(
"\n",
"Paragraph One",
"",
"# Header 1",
"",
"Paragraph Two");
string expected = string.Join(
"\n",
"Paragraph One",
"",
"# Header 1",
"",
"Paragraph Two");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void FollowedByText()
{
string input = string.Join(
"\n",
"# Header 1",
"Text Block");
string expected = string.Join(
"\n",
"# Header 1",
"",
"Text Block");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void H1()
{
string input = "# Header 1";
string expected = "# Header 1";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void H2()
{
string input = "## Header 2";
string expected = "## Header 2";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void H3()
{
string input = "### Header 3";
string expected = "### Header 3";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void H4()
{
string input = "#### Header 4";
string expected = "Header 4";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void H5()
{
string input = "##### Header 5";
string expected = "Header 5";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void H6()
{
string input = "###### Header 6";
string expected = "Header 6";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,28 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class ImageLinkTests
{
[Fact]
public void NamedImageLink()
{
string input = "![test](image.jpg)";
string expected = "=> image.jpg test";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void SimpleImageLink()
{
string input = "![](image.jpg)";
string expected = "=> image.jpg";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,106 @@
using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class IncreaseHeaderDepthsAfterFirstTests
{
private readonly string one1s;
private readonly string two1s;
public IncreaseHeaderDepthsAfterFirstTests()
{
this.two1s = string.Join(
"\n",
"# Heading 1",
"",
"Line",
"",
"# Heading 2");
this.one1s = string.Join(
"\n",
"# Heading 1",
"",
"Line",
"",
"## Heading 2");
}
[Fact]
public void WithOne1s()
{
string expected = string.Join(
"\n",
"# Heading 1",
"",
"Line",
"",
"## Heading 2");
string actual = MarkdownGemtext.ToGemtext(
this.one1s,
new MarkdownPipelineBuilder()
.Use<IncreaseHeaderDepthsAfterFirst>()
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void WithoutOne1s()
{
string expected = string.Join(
"\n",
"# Heading 1",
"",
"Line",
"",
"## Heading 2");
string actual = MarkdownGemtext.ToGemtext(
this.one1s,
new MarkdownPipelineBuilder()
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void WithoutTwo1s()
{
string expected = string.Join(
"\n",
"# Heading 1",
"",
"Line",
"",
"# Heading 2");
string actual = MarkdownGemtext.ToGemtext(
this.two1s,
new MarkdownPipelineBuilder()
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void WithTwo1s()
{
string expected = string.Join(
"\n",
"# Heading 1",
"",
"Line",
"",
"## Heading 2");
string actual = MarkdownGemtext.ToGemtext(
this.two1s,
new MarkdownPipelineBuilder()
.Use<IncreaseHeaderDepthsAfterFirst>()
.Build());
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,64 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class LinkTests
{
[Fact]
public void BareLink()
{
string input = "[](/url)";
string expected = "=> /url";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void ListOfLinks()
{
string input = string.Join(
"\n",
"- [](/a)",
"- [Two](/b)",
"- [Three](/c/d)");
string expected = string.Join(
"\n",
"=> /a",
"=> /b Two",
"=> /c/d Three");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void MixedLinkOfLists()
{
string input = string.Join(
"\n",
"- [](/a)",
"- Two",
"- [Three](/c/d)");
string expected = string.Join(
"\n",
"=> /a",
"* Two",
"=> /c/d Three");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void PlainLink()
{
string input = "[test](/url)";
string expected = "=> /url test";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,26 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class ListTests
{
[Fact(Skip = "Not ready to figure this out.")]
public void NestedLists()
{
string input = string.Join(
"\n",
"- A",
" - B",
"- C");
string expected = string.Join(
"\n",
"* A",
"* B",
"* C");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>MfGames.Markdown.Gemini.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Markdown.Gemtext\MfGames.Markdown.Gemtext.csproj"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,164 @@
using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
/// <summary>
/// Tests the various functionality of the ParagraphLinkHandling and
/// EndLinkInlineFormatting logic works.
/// </summary>
public class ParagraphLinkHandlingTests
{
private readonly string input;
public ParagraphLinkHandlingTests()
{
this.input = string.Join(
"\n",
"This is a paragraph with an [inline](https://example.com) link.",
"Here is [another](https://example.org/) link, part of the same paragraph.",
"",
"This is a second paragraph, with a different [link](https://duck.com) in it.");
}
[Fact]
public void DefaultHandling()
{
string expected = string.Join(
"\n",
"This is a paragraph with an",
"=> https://example.com inline",
"link. Here is",
"=> https://example.org/ another",
"link, part of the same paragraph.",
"",
"This is a second paragraph, with a different",
"=> https://duck.com link",
"in it.");
string actual = MarkdownGemtext.ToGemtext(
this.input,
new MarkdownPipelineBuilder()
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void DocumentEndHandling()
{
string expected = string.Join(
"\n",
"This is a paragraph with an inline[1] link. Here is another[2] link, part of the same paragraph.",
"",
"This is a second paragraph, with a different link[3] in it.",
"",
"=> https://example.com 1: https://example.com",
"=> https://example.org/ 2: https://example.org/",
"=> https://duck.com 3: https://duck.com");
string actual = MarkdownGemtext.ToGemtext(
this.input,
new MarkdownPipelineBuilder()
.Use(
new SetBlockLinkHandling(
BlockLinkHandling.DocumentEnd))
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void ParagraphEndHandling()
{
string expected = string.Join(
"\n",
"This is a paragraph with an inline[1] link. Here is another[2] link, part of the same paragraph.",
"",
"=> https://example.com 1: https://example.com",
"=> https://example.org/ 2: https://example.org/",
"",
"This is a second paragraph, with a different link[3] in it.",
"",
"=> https://duck.com 3: https://duck.com");
string actual = MarkdownGemtext.ToGemtext(
this.input,
new MarkdownPipelineBuilder()
.Use(
new SetBlockLinkHandling(
BlockLinkHandling.ParagraphEnd))
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void ParagraphEndHandlingWithFootnote()
{
string expected = string.Join(
"\n",
"This is a paragraph with an inline[10] link. Here is another[11] link, part of the same paragraph.",
"",
"=> https://example.com 10: https://example.com",
"=> https://example.org/ 11: https://example.org/",
"",
"This is a second paragraph, with a different link[12] in it.",
"",
"=> https://duck.com 12: https://duck.com");
string actual = MarkdownGemtext.ToGemtext(
this.input,
new MarkdownPipelineBuilder()
.Use(
new SetBlockLinkHandling(
BlockLinkHandling.ParagraphEnd,
nextFootnoteNumber: 10))
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void ParagraphEndHandlingWithTextLinks()
{
string expected = string.Join(
"\n",
"This is a paragraph with an inline link. Here is another link, part of the same paragraph.",
"",
"=> https://example.com inline",
"=> https://example.org/ another",
"",
"This is a second paragraph, with a different link in it.",
"",
"=> https://duck.com link");
string actual = MarkdownGemtext.ToGemtext(
this.input,
new MarkdownPipelineBuilder()
.Use(
new SetBlockLinkHandling(
BlockLinkHandling.ParagraphEnd,
EndLinkInlineFormatting.Text))
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveHandling()
{
string expected = string.Join(
"\n",
"This is a paragraph with an inline link. "
+ "Here is another link, part of the same paragraph.",
"",
"This is a second paragraph, with a different link in it.");
string actual = MarkdownGemtext.ToGemtext(
this.input,
new MarkdownPipelineBuilder()
.Use(new SetBlockLinkHandling(BlockLinkHandling.Remove))
.Build());
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,60 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class PlainTextTests
{
[Fact]
public void NormalText()
{
string input = "This is input.";
string expected = "This is input.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void Paragraphs()
{
// Okay, this one isn't based on the test_plain.py, but seems
// useful to have while converting logic.
string input = string.Join(
"\n",
"Paragraph One",
string.Empty,
"Paragraph Two");
string expected = string.Join(
"\n",
"Paragraph One",
string.Empty,
"Paragraph Two");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void SoftBreakParagraphs()
{
// Okay, this one isn't based on the test_plain.py, but seems
// useful to have while converting logic.
string input = string.Join(
"\n",
"Paragraph",
"One",
" Three",
string.Empty,
"Paragraph Two");
string expected = string.Join(
"\n",
"Paragraph One Three",
string.Empty,
"Paragraph Two");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,31 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired
{
/// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_base_url.py
/// </summary>
public class BaseUrlPythonTests
{
[Fact]
public void AbsoluteUrl()
{
string input = "[test](https://duck.com/test)";
string expected = "=> https://duck.com/test test";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RootUrl()
{
string input = "[test](/url)";
string expected = "=> /url test";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,81 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired
{
/// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_codeblock.py
/// </summary>
public class CodeBlockPythonTests
{
[Fact]
public void EndsWithNoNewLines()
{
string input = string.Join(
"\n",
"Non code block",
"",
" code block here");
string expected = string.Join(
"\n",
"Non code block",
"",
"```",
"code block here",
"```");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void NoExtraNewline()
{
string input = string.Join(
"\n",
"Non code block",
"",
" code block here",
"",
"More non code");
string expected = string.Join(
"\n",
"Non code block",
"",
"```",
"code block here",
"```",
"",
"More non code");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact(Skip = "Not sure how to implement or justification of this one")]
public void WithExtraNewline()
{
string input = string.Join(
"\n",
"Non code block",
"",
" code block here",
"",
"",
"More non code");
string expected = string.Join(
"\n",
"Non code block",
"",
"```",
"code block here",
"",
"```",
"",
"More non code");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,47 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired
{
/// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_list.py
/// </summary>
public class ListPythonTests
{
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
public void ListWithNewlineAndSpaces(int request)
{
string padding = new string(' ', request);
string input = $"- hello\n{padding}world\n- oh";
string expected = "* hello world\n* oh";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void NormalList()
{
string input = "- hi\n- oh";
string expected = "* hi\n* oh";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void SeparateLists()
{
string input = "* hello\n\n- oh";
string expected = "* hello\n\n* oh";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,104 @@
using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired
{
/// <summary>
/// Runs through various plain input tests.
/// Based on
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_plain.py
/// </summary>
public class PlainTextPythonTests
{
[Fact]
public void ConvertHtmlBlock()
{
string input = string.Join(
"\n",
"<div>",
"<tag>html is here</tag>",
"</div>",
"",
"Text after");
string expected = string.Join(
"\n",
"```html",
"<div>",
"<tag>html is here</tag>",
"</div>",
"```",
"",
"Text after");
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use<HtmlAsCodeBlocks>()
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void EmptyInput()
{
string input = "";
string expected = "";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveBold()
{
string input = "Sentence with **bold** in it.";
string expected = "Sentence with bold in it.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveBold2()
{
string input = "Sentence with __bold__ in it.";
string expected = "Sentence with bold in it.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveInlineHtml()
{
string input =
"<i>test1</i> <b>test2</b> <em>test3</em> <made-up>test4</made-up>";
string expected = "test1 test2 test3 test4";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveItalics()
{
string input = "Sentence with *italics* in it.";
string expected = "Sentence with italics in it.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveItalics2()
{
string input = "Sentence with _italics_ in it.";
string expected = "Sentence with italics in it.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,6 @@
# Python-Inpsired Test
These tests come
from [md2gemini](https://github.com/makeworld-the-better-one/md2gemini/tree/master/tests)
but the logic for how to generate them does not. We are simply using them as a
basic test case and then alterating Markdig to match.

View File

@ -0,0 +1,110 @@
using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired
{
/// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_strip_html.py
/// </summary>
public class StripHtmlPythonTests
{
[Fact]
public void KeepBold()
{
string input = "Sentence with **bold** in it.";
string expected = "Sentence with **bold** in it.";
var inlineFormatting = new SetInlineFormatting()
{
Default = InlineFormatting.Normalize,
};
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use(inlineFormatting)
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void KeepBold2()
{
string input = "Sentence with __bold__ in it.";
string expected = "Sentence with **bold** in it.";
var inlineFormatting = new SetInlineFormatting()
{
Default = InlineFormatting.Normalize,
};
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use(inlineFormatting)
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void KeepItalics()
{
string input = "Sentence with *italics* in it.";
string expected = "Sentence with *italics* in it.";
var inlineFormatting = new SetInlineFormatting()
{
Default = InlineFormatting.Normalize,
};
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use(inlineFormatting)
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void KeepItalics2()
{
string input = "Sentence with _italics_ in it.";
string expected = "Sentence with *italics* in it.";
var inlineFormatting = new SetInlineFormatting()
{
Default = InlineFormatting.Normalize,
};
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use(inlineFormatting)
.Build());
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveHtmlBlock()
{
string input =
"<div>\n<tag>html is here</tag>\n</div>\n\nText after";
string expected = "Text after";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void RemoveInlineHtml()
{
string input =
"<i>test1</i> <b>test2</b> <em>test3</em> <made-up>test4</made-up>";
string expected = "test1 test2 test3 test4";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,37 @@
using Markdig;
using Markdig.Extensions.SmartyPants;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class QuoteTests
{
[Fact]
public void NormalDouble()
{
string input = "Normal \"double\" quote.";
string expected = "Normal \"double\" quote.";
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
[Fact]
public void SmartyDouble()
{
GemtextSmartyPantsExtension smartyPants =
new(new SmartyPantOptions());
string input = "Normal \"double\" quote.";
string expected = "Normal “double” quote.";
string actual = MarkdownGemtext.ToGemtext(
input,
new MarkdownPipelineBuilder()
.Use(smartyPants)
.Build());
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,31 @@
using MfGames.Markdown.Gemtext;
using Xunit;
namespace MfGames.Markdown.Gemini.Tests
{
public class TableTests
{
[Fact(Skip = "Tables are out of scope at this point")]
public void SimpleImageLink()
{
string input = string.Join(
"\n",
"a|b|c",
"-|-|",
"1|2|3",
"4|5|6");
string expected = string.Join(
"\n",
"┌───┬───┬───┐",
"│ a │ b │ c │",
"╞═══╪═══╪═══╡",
"│ 1 │ 2 │ 3 │",
"├───┼───┼───┤",
"│ 4 │ 5 │ 6 │",
"└───┴───┴───┘");
string actual = MarkdownGemtext.ToGemtext(input);
Assert.Equal(expected, actual);
}
}
}

View File

@ -0,0 +1,54 @@
using Markdig;
using Markdig.Extensions.SmartyPants;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Extensions
{
/// <summary>
/// Extension to enable SmartyPants, but for Gemtext.
/// </summary>
public class GemtextSmartyPantsExtension : IMarkdownExtension
{
/// <summary>
/// Initializes a new instance of the <see cref="SmartyPantsExtension" /> class.
/// </summary>
/// <param name="options">The options.</param>
public GemtextSmartyPantsExtension(SmartyPantOptions? options)
{
this.Options = options ?? new SmartyPantOptions();
}
/// <summary>
/// Gets the options.
/// </summary>
public SmartyPantOptions Options { get; }
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<SmartyPantsInlineParser>())
{
// Insert the parser after the code span parser
pipeline.InlineParsers.InsertAfter<CodeInlineParser>(
new SmartyPantsInlineParser());
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtextRenderer)
{
return;
}
if (!gemtextRenderer.ObjectRenderers
.Contains<GemtextSmartyPantRenderer>())
{
gemtextRenderer.ObjectRenderers.Add(
new GemtextSmartyPantRenderer(this.Options));
}
}
}
}

View File

@ -0,0 +1,28 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions
{
/// <summary>
/// Extension method to retain HTML blocks as a code fenced block with
/// "html" as the data.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class HtmlAsCodeBlocks : IMarkdownExtension
{
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is GemtextRenderer gemtext)
{
gemtext.HtmlBlockFormatting = HtmlBlockFormatting.CodeBlock;
}
}
}
}

View File

@ -0,0 +1,37 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
namespace MfGames.Markdown.Gemtext.Extensions
{
/// <summary>
/// Extension method to control the depth of the headers in a file so that
/// the first one (maybe a header) is H1 but the others are decreased to
/// H2 or lower depending on their initial level.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class IncreaseHeaderDepthsAfterFirst : IMarkdownExtension
{
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
var heading = gemtext.ObjectRenderers.Find<HeadingRenderer>();
if (heading != null)
{
heading.IncreaseHeaderDepthAfterFirst = true;
}
}
}
}

View File

@ -0,0 +1,63 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions
{
/// <summary>
/// Extension method to control how links are processed inside blocks.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class SetBlockLinkHandling : IMarkdownExtension
{
public SetBlockLinkHandling(
BlockLinkHandling? blockLinkHandling = null,
EndLinkInlineFormatting? endLinkInlineFormatting = null,
int? nextFootnoteNumber = null)
{
this.BlockLinkHandling = blockLinkHandling;
this.EndLinkInlineFormatting = endLinkInlineFormatting;
this.NextFootnoteNumber = nextFootnoteNumber;
}
/// <summary>
/// Gets or sets how block links are handled. If this is null, then no
/// change is made to the current renderer.
/// </summary>
public BlockLinkHandling? BlockLinkHandling { get; set; }
/// <summary>
/// Gets or sets how links are formatted if they are gathered to the
/// end of the paragraph or document. If this is null, then no change
/// will be made.
/// </summary>
public EndLinkInlineFormatting? EndLinkInlineFormatting { get; set; }
/// <summary>
/// Gets or sets the next footnote number. If this is null, then no
/// change will be made.
/// </summary>
public int? NextFootnoteNumber { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
gemtext.BlockLinkHandling = this.BlockLinkHandling
?? gemtext.BlockLinkHandling;
gemtext.EndLinkInlineFormatting = this.EndLinkInlineFormatting
?? gemtext.EndLinkInlineFormatting;
gemtext.NextFootnoteNumber = this.NextFootnoteNumber
?? gemtext.NextFootnoteNumber;
}
}
}

View File

@ -0,0 +1,49 @@
using Markdig;
using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions
{
/// <summary>
/// Extension method to turn all inline formatting from the default of
/// removing to render in normalizing in rendered form.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class SetInlineFormatting : IMarkdownExtension
{
/// <summary>
/// Gets or sets the override formatting for code lines
/// (backtick) spans.
/// </summary>
public InlineFormatting? Code { get; set; }
/// <summary>
/// Sets or sets the override formatting for all inlines.
/// </summary>
public InlineFormatting? Default { get; set; }
/// <summary>
/// Gets or sets the override formatting for emphasis (italic
/// and bold) spans.
/// </summary>
public InlineFormatting? Emphasis { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not GemtextRenderer gemtext)
{
return;
}
gemtext.InlineFormatting = this.Default ?? gemtext.InlineFormatting;
gemtext.EmphasisFormatting = this.Emphasis;
gemtext.CodeFormatting = this.Code;
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.IO;
using Markdig;
using Markdig.Parsers;
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext
{
/// <summary>
/// The static class that corresponds to Markdig.Markdown. This is written
/// with the same pattern, but since `Markdown` is a static, we can't tack
/// onto that.
/// </summary>
public static class MarkdownGemtext
{
private static readonly MarkdownPipeline DefaultPipeline;
static MarkdownGemtext()
{
DefaultPipeline = new MarkdownPipelineBuilder()
.Build();
}
/// <summary>
/// Converts the given Markdown
/// </summary>
/// <param name="markdown">A Markdown text.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <param name="context">A parser context used for the parsing.</param>
/// <returns>The result of the conversion</returns>
public static string ToGemtext(
string markdown,
MarkdownPipeline? pipeline = null,
MarkdownParserContext? context = null)
{
if (markdown == null)
{
throw new ArgumentNullException(nameof(markdown));
}
pipeline ??= DefaultPipeline;
MarkdownDocument document = MarkdownParser
.Parse(markdown, pipeline, context);
return ToGemtext(document, pipeline);
}
/// <summary>
/// Converts a Markdown document to HTML.
/// </summary>
/// <param name="document">A Markdown document.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
public static string ToGemtext(
this MarkdownDocument document,
MarkdownPipeline? pipeline = null)
{
// Make sure we have sane parameters.
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
pipeline ??= DefaultPipeline;
// Set up the writer to contain the markdown and the Gemtext
// renderer.
var writer = new StringWriter();
GemtextRenderer renderer = new(writer);
pipeline.Setup(renderer);
// Render the Markdown into Gemtext and re turn the results.
renderer.Render(document);
renderer.Writer.Flush();
return renderer.Writer.ToString() ?? string.Empty;
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.25.0"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
namespace MfGames.Markdown.Gemtext.Renderers
{
/// <summary>
/// Describes how links are processed within a paragraph.
/// </summary>
public enum BlockLinkHandling
{
/// <summary>
/// Indicates that the paragraph should be broken apart and the link
/// included on its own line in the middle of the paragraph.
/// </summary>
InsertLine,
/// <summary>
/// Indicates that all the links in a paragraph should be gathered
/// and then emitted at the end of the paragraph. The text of the link
/// will be left in the paragraph.
/// </summary>
ParagraphEnd,
/// <summary>
/// Indicates that all the links in the document should be gathered
/// and then emitted at the end of the document. The text of the link
/// will be left in the paragraph.
/// </summary>
DocumentEnd,
/// <summary>
/// Indicates that the links themselves should be removed and just the
/// text included in the paragraph.
/// </summary>
Remove,
}
}

View File

@ -0,0 +1,22 @@
namespace MfGames.Markdown.Gemtext.Renderers
{
/// <summary>
/// Describes how a paragraph link is formatted inside the text. This is
/// only used for `ParagraphLinkHandling.ParagraphEnd` and
/// `ParagraphLinkHandling.DocumentEnd`.
/// </summary>
public enum EndLinkInlineFormatting
{
/// <summary>
/// Indicates that a footnote notation (`[1]`) will be insert into the
/// text and then the link will be displayed with the URL when gathered.
/// </summary>
Footnote,
/// <summary>
/// Indicates that the text is put in as-is into the gathered link with
/// no footnote given in the block.
/// </summary>
Text,
}
}

View File

@ -0,0 +1,39 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// An Gemtext renderer for a <see cref="CodeBlock" /> and
/// <see cref="FencedCodeBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{CodeBlock}" />
public class CodeBlockRenderer : GemtextObjectRenderer<CodeBlock>
{
protected override void Write(GemtextRenderer renderer, CodeBlock obj)
{
// We need to have two lines above this.
renderer.EnsureTwoLines();
// Code blocks are always fenced, but we allow for additional text
// at the end of them which is only in `FencedCodeBlock`.
if (obj is FencedCodeBlock fenced)
{
renderer.WriteLine("```" + fenced.Info);
}
else
{
renderer.WriteLine("```");
}
renderer.WriteLeafRawLines(obj, true);
renderer.Write("```");
// If we aren't at the end of the container, then add some spacing.
if (!renderer.IsLastInContainer)
{
renderer.WriteLine();
renderer.WriteLine();
}
}
}
}

View File

@ -0,0 +1,56 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// An Gemtext renderer for a <see cref="HeadingBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{HeadingBlock}" />
public class HeadingRenderer : GemtextObjectRenderer<HeadingBlock>
{
private int currentHeading;
/// <summary>
/// Gets or sets a value indicating whether the header depths are
/// increased after the first one.
/// </summary>
public bool IncreaseHeaderDepthAfterFirst { get; set; }
protected override void Write(
GemtextRenderer renderer,
HeadingBlock obj)
{
// Figure out the level we should be processing.
int level = obj.Level;
if (this.currentHeading++ > 0 && this.IncreaseHeaderDepthAfterFirst)
{
// Check the second header we see. If this header is H2 or
// higher, then we assume that the file has been already updated
// to handle the heading and we stop processing.
if (this.currentHeading == 2 && level != 1)
{
this.IncreaseHeaderDepthAfterFirst = false;
}
else
{
// We are bumping the heading levels up.
level++;
}
}
// Write out the prefix of the header.
string prefix = level switch
{
1 => "# ",
2 => "## ",
3 => "### ",
_ => "",
};
renderer.EnsureTwoLines();
renderer.Write(prefix);
renderer.WriteLeafInline(obj);
}
}
}

View File

@ -0,0 +1,33 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// A Gemtext renderer for a <see cref="GemtextBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{GemtextBlock}" />
public class HtmlBlockRenderer : GemtextObjectRenderer<HtmlBlock>
{
protected override void Write(GemtextRenderer renderer, HtmlBlock obj)
{
// If we are stripping out HTML blocks (default), then nothing to
// do with rendering.
if (renderer.HtmlBlockFormatting == HtmlBlockFormatting.Remove)
{
return;
}
// Otherwise, we treat this as a fenced code block.
renderer.EnsureTwoLines();
renderer.WriteLine("```html");
renderer.WriteLeafRawLines(obj, true);
renderer.WriteLine("```");
// If we aren't at the end of the container, then add some spacing.
if (!renderer.IsLastInContainer)
{
renderer.WriteLine();
}
}
}
}

View File

@ -0,0 +1,35 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// A Gemtext renderer for a <see cref="ListBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ListBlock}" />
public class ListRenderer : GemtextObjectRenderer<ListBlock>
{
protected override void Write(
GemtextRenderer renderer,
ListBlock listBlock)
{
// Lists need to be separated from the rest.
renderer.EnsureTwoLines();
// Go through each list item and write them out.
foreach (var item in listBlock)
{
// If the list only contains a link, then we just render the
// link instead.
var listItem = (ListItemBlock)item;
if (!listItem.OnlyHasSingleLink())
{
renderer.EnsureLine();
renderer.Write("* ");
}
renderer.WriteChildren(listItem);
}
}
}
}

View File

@ -0,0 +1,27 @@
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// A Gemtext renderer for a <see cref="MarkdownDocument" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ParagraphBlock}" />
public class MarkdownDocumentRenderer
: GemtextObjectRenderer<MarkdownDocument>
{
protected override void Write(
GemtextRenderer renderer,
MarkdownDocument obj)
{
// Simply write out the contents.
renderer.WriteChildren(obj);
// If we get to the end of the document and we have gathered links,
// and we are in DocumentEnd mode, then write out the links. We
// don't test for the mode here because if there are links, we
// should write them out.
LinkInlineRenderer.WriteGatheredLinks(renderer);
}
}
}

View File

@ -0,0 +1,43 @@
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// A Gemtext renderer for a <see cref="ParagraphBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ParagraphBlock}" />
public class ParagraphRenderer : GemtextObjectRenderer<ParagraphBlock>
{
protected override void Write(
GemtextRenderer renderer,
ParagraphBlock obj)
{
// If we aren't the first in the container, we need to break apart
// the lines to make it easier to read.
if (!renderer.IsFirstInContainer)
{
renderer.EnsureTwoLines();
}
// We need to save the state of the link rendering while handling
// this block.
if (obj.OnlyHasSingleLink())
{
renderer.WriteLeafInline(obj);
}
else
{
renderer.WhileLinkInsideBlock(
() => renderer.WriteLeafInline(obj));
}
// If we get to the end of the paragraph and we have gathered links,
// and we are in ParagraphEnd mode, then write out the links.
if (renderer.BlockLinkHandling == BlockLinkHandling.ParagraphEnd)
{
LinkInlineRenderer.WriteGatheredLinks(renderer);
}
}
}
}

View File

@ -0,0 +1,21 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// A Gemtext renderer for a <see cref="QuoteBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{QuoteBlock}" />
public class QuoteBlockRenderer : GemtextObjectRenderer<QuoteBlock>
{
protected override void Write(GemtextRenderer renderer, QuoteBlock obj)
{
string quoteIndent = obj.QuoteChar + " ";
renderer.EnsureTwoLines();
renderer.PushIndent(quoteIndent);
renderer.WriteChildren(obj);
renderer.PopIndent();
}
}
}

View File

@ -0,0 +1,20 @@
using Markdig.Syntax;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
{
/// <summary>
/// A Gemtext renderer for a <see cref="ThematicBreakBlock" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{ThematicBreakBlock}" />
public class ThematicBreakRenderer
: GemtextObjectRenderer<ThematicBreakBlock>
{
protected override void Write(
GemtextRenderer renderer,
ThematicBreakBlock obj)
{
renderer.EnsureTwoLines();
renderer.WriteLine("---");
}
}
}

View File

@ -0,0 +1,18 @@
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext
{
/// <summary>
/// A base class for Gemtext rendering <see cref="Block" /> and
/// <see cref="Inline" /> Markdown objects.
/// </summary>
/// <typeparam name="TObject">The type of the object.</typeparam>
/// <seealso cref="IMarkdownObjectRenderer" />
public abstract class GemtextObjectRenderer<TObject>
: MarkdownObjectRenderer<GemtextRenderer, TObject>
where TObject : MarkdownObject
{
}
}

View File

@ -0,0 +1,30 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="CodeInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{CodeInline}" />
public class CodeInlineRenderer : GemtextObjectRenderer<CodeInline>
{
protected override void Write(GemtextRenderer renderer, CodeInline obj)
{
const string Delimiter = "`";
InlineFormatting formatting = renderer.CodeFormattingResolved;
bool normalize = formatting == InlineFormatting.Normalize;
if (normalize)
{
renderer.Write(Delimiter);
}
renderer.Write(obj.Content);
if (normalize)
{
renderer.Write(Delimiter);
}
}
}
}

View File

@ -0,0 +1,20 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="DelimiterInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{DelimiterInline}" />
public class DelimiterInlineRenderer
: GemtextObjectRenderer<DelimiterInline>
{
protected override void Write(
GemtextRenderer renderer,
DelimiterInline obj)
{
renderer.Write(obj.ToLiteral());
renderer.WriteChildren(obj);
}
}
}

View File

@ -0,0 +1,32 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for an <see cref="EmphasisInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{EmphasisInline}" />
public class EmphasisInlineRenderer : GemtextObjectRenderer<EmphasisInline>
{
protected override void Write(
GemtextRenderer renderer,
EmphasisInline obj)
{
InlineFormatting formatting = renderer.EmphasisFormattingResolved;
bool normalize = formatting == InlineFormatting.Normalize;
string delimiter = new string('*', obj.DelimiterCount);
if (normalize)
{
renderer.Write(delimiter);
}
renderer.WriteChildren(obj);
if (normalize)
{
renderer.Write(delimiter);
}
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Net;
using Markdig.Extensions.SmartyPants;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="SmartyPant" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{GemtextEntityInline}" />
public class GemtextSmartyPantRenderer
: GemtextObjectRenderer<SmartyPant>
{
private static readonly SmartyPantOptions DefaultOptions = new();
private readonly SmartyPantOptions options;
/// <summary>
/// Initializes a new instance of the <see cref="HtmlSmartyPantRenderer" /> class.
/// </summary>
/// <param name="options">The options.</param>
/// <exception cref="ArgumentNullException"></exception>
public GemtextSmartyPantRenderer(SmartyPantOptions? options)
{
this.options = options
?? throw new ArgumentNullException(nameof(options));
}
protected override void Write(GemtextRenderer renderer, SmartyPant obj)
{
if (!this.options.Mapping.TryGetValue(obj.Type, out string? text))
{
DefaultOptions.Mapping.TryGetValue(obj.Type, out text);
}
string? unicode = WebUtility.HtmlDecode(text);
renderer.Write(unicode);
}
}
}

View File

@ -0,0 +1,19 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="GemtextEntityInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{GemtextEntityInline}" />
public class HtmlEntityInlineRenderer
: GemtextObjectRenderer<HtmlEntityInline>
{
protected override void Write(
GemtextRenderer renderer,
HtmlEntityInline obj)
{
renderer.Write(obj.Transcoded);
}
}
}

View File

@ -0,0 +1,32 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="LineBreakInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{LineBreakInline}" />
public class LineBreakInlineRenderer
: GemtextObjectRenderer<LineBreakInline>
{
/// <summary>
/// Gets or sets a value indicating whether to render this softline break as a
/// Gemtext hardline break tag (&lt;br /&gt;)
/// </summary>
public bool RenderAsHardlineBreak { get; set; }
protected override void Write(
GemtextRenderer renderer,
LineBreakInline obj)
{
if (obj.IsHard || this.RenderAsHardlineBreak)
{
renderer.EnsureTwoLines();
}
else
{
renderer.Write(" ");
}
}
}
}

View File

@ -0,0 +1,124 @@
using System.IO;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="LinkInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{LinkInline}" />
public class LinkInlineRenderer : GemtextObjectRenderer<LinkInline>
{
/// <summary>
/// Writes out any gathered links in a block.
/// </summary>
/// <param name="renderer">The renderer being used.</param>
public static void WriteGatheredLinks(GemtextRenderer renderer)
{
// If we have no gathered links, then there is nothing to do.
if (renderer.GatheredLinks.Count <= 0)
{
return;
}
// Put some space between the previous object and this one, then
// write out each link which is already formatted.
renderer.WriteLine();
foreach (var link in renderer.GatheredLinks)
{
renderer.WriteLine();
renderer.Write(link);
}
// Clear out the list of links.
renderer.GatheredLinks.Clear();
}
protected override void Write(GemtextRenderer renderer, LinkInline link)
{
// Figure out the various states we have.
bool outside = !renderer.LinkInsideBlock;
bool insert = !outside
&& renderer.BlockLinkHandling == BlockLinkHandling.InsertLine;
bool gather = !outside
&& renderer.BlockLinkHandling switch
{
BlockLinkHandling.DocumentEnd => true,
BlockLinkHandling.ParagraphEnd => true,
_ => false,
};
bool hasText = link.FirstChild != null;
bool footnotes = renderer.EndLinkInlineFormatting
== EndLinkInlineFormatting.Footnote;
// Bare links and ones where we insert into the paragraph have
// their own line.
string? url = link.GetDynamicUrl != null
? link.GetDynamicUrl() ?? link.Url
: link.Url;
if (outside || insert)
{
// Make sure we are at the beginning of the line before
// rendering the link.
renderer.EnsureLine();
renderer.Write("=> ");
renderer.Write(url);
// If we have text, we need a space after the URL and before
// the text.
if (hasText)
{
renderer.Write(" ");
}
}
// Render the text for the link if we have it.
if (hasText)
{
renderer.WriteChildren(link);
}
// If we are gathering, then write out a footnote.
if (gather)
{
int footnoteNumber = renderer.NextFootnoteNumber++;
string linkText = footnotes
? footnoteNumber + ": " + url
: GetLinkText(link);
if (footnotes)
{
renderer.Write($"[{footnoteNumber}]");
}
renderer.GatheredLinks.Add("=> " + url + " " + linkText);
}
// If we are inserting a line in the paragraph, we need a final
// newline so the text of the paragraph continues on the next
// line.
if (insert)
{
renderer.WriteLine();
}
}
private static string GetLinkText(LinkInline link)
{
// This little bit of nasty code basically spins up a new renderer
// to get the text of the link by itself. Then we return that
// directly so it can be rendered as a link.
StringWriter writer = new();
GemtextRenderer renderer = new(writer);
renderer.WriteChildren(link);
writer.Close();
string text = writer.ToString();
return text;
}
}
}

View File

@ -0,0 +1,39 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
{
/// <summary>
/// A Gemtext renderer for a <see cref="LiteralInline" />.
/// </summary>
/// <seealso cref="GemtextObjectRenderer{LiteralInline}" />
public class LiteralInlineRenderer : GemtextObjectRenderer<LiteralInline>
{
protected override void Write(
GemtextRenderer renderer,
LiteralInline obj)
{
// If we are inside a paragraph and we are doing inline formatting,
// then we need to trim the text if we are before or after a link.
string content = obj.Content.ToString();
BlockLinkHandling handling = renderer.BlockLinkHandling;
bool isInsert = handling == BlockLinkHandling.InsertLine;
bool inBlock = renderer.LinkInsideBlock;
if (inBlock && isInsert)
{
if (obj.PreviousSibling is LinkInline)
{
content = content.TrimStart();
}
if (obj.NextSibling is LinkInline)
{
content = content.TrimEnd();
}
}
// Write out the manipulated content.
renderer.Write(content);
}
}
}

View File

@ -0,0 +1,36 @@
using System.Linq;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext
{
/// <summary>
/// Various useful extension methods for Markdig classes.
/// </summary>
public static class MarkdigExtensions
{
/// <summary>
/// Determines if the paragraph only contains a link.
/// </summary>
/// <param name="obj">The object to inspect.</param>
/// <returns>True if there is only a link in the paragraph.</returns>
public static bool OnlyHasSingleLink(this ParagraphBlock obj)
{
return obj.Inline != null
&& obj.Inline.Count() == 1
&& obj.Inline.FirstChild is LinkInline;
}
/// <summary>
/// Determines if the list item only contains a link.
/// </summary>
/// <param name="obj">The object to inspect.</param>
/// <returns>True if there is only a link in the paragraph.</returns>
public static bool OnlyHasSingleLink(this ListItemBlock obj)
{
return obj.Count == 1
&& obj.LastChild is ParagraphBlock paragraphBlock
&& paragraphBlock.OnlyHasSingleLink();
}
}
}

View File

@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.IO;
using Markdig.Helpers;
using Markdig.Renderers;
using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers
{
public class GemtextRenderer : TextRendererBase<GemtextRenderer>
{
/// <inheritdoc />
public GemtextRenderer(TextWriter writer)
: base(writer)
{
// Set up our default values.
this.NextFootnoteNumber = 1;
this.GatheredLinks = new List<string>();
// Default block renderers.
this.ObjectRenderers.Add(new CodeBlockRenderer());
this.ObjectRenderers.Add(new HeadingRenderer());
this.ObjectRenderers.Add(new HtmlBlockRenderer());
this.ObjectRenderers.Add(new ListRenderer());
this.ObjectRenderers.Add(new MarkdownDocumentRenderer());
this.ObjectRenderers.Add(new ParagraphRenderer());
this.ObjectRenderers.Add(new QuoteBlockRenderer());
this.ObjectRenderers.Add(new ThematicBreakRenderer());
// Default inline renderers.
this.ObjectRenderers.Add(new CodeInlineRenderer());
this.ObjectRenderers.Add(new DelimiterInlineRenderer());
this.ObjectRenderers.Add(new EmphasisInlineRenderer());
this.ObjectRenderers.Add(new HtmlEntityInlineRenderer());
this.ObjectRenderers.Add(new LineBreakInlineRenderer());
this.ObjectRenderers.Add(new LinkInlineRenderer());
this.ObjectRenderers.Add(new LiteralInlineRenderer());
}
/// <summary>
/// Gets or sets how to handle links inside paragraphs and other blocks.
/// </summary>
public BlockLinkHandling BlockLinkHandling { get; set; }
/// <summary>
/// Gets or sets the optional formatting for code inlines (backticks).
/// If this is unset, then `InlineFormatting` will be used.
/// </summary>
public InlineFormatting? CodeFormatting { get; set; }
/// <summary>
/// Gets the actual formatting for code inlines (backticks) which
/// is either `CodeInlineFormatting` or `InlineFormatting` if that
/// is not set.
/// </summary>
public InlineFormatting CodeFormattingResolved =>
this.CodeFormatting ?? this.InlineFormatting;
/// <summary>
/// Gets or sets the optional formatting for emphasis (which includes
/// italics and bolds). If this is unset, then `InlineFormatting`
/// will be used.
/// </summary>
public InlineFormatting? EmphasisFormatting { get; set; }
/// <summary>
/// Gets the actual formatting for emphasis which is either
/// `EmphasisFormatting` or `InlineFormatting` if that isn't set.
/// </summary>
public InlineFormatting EmphasisFormattingResolved =>
this.EmphasisFormatting ?? this.InlineFormatting;
/// <summary>
/// Gets or sets the formatting for how links that are gathered at the
/// end of a paragraph or document are formatted inside the paragraph.
/// </summary>
public EndLinkInlineFormatting EndLinkInlineFormatting { get; set; }
/// <summary>
/// Gets the current list of formatted links that have been gathered
/// up to this point for rendering.
/// </summary>
public List<string> GatheredLinks { get; }
/// <summary>
/// Gets or sets the formatting rule for HTML blocks.
/// </summary>
public HtmlBlockFormatting HtmlBlockFormatting { get; set; }
/// <summary>
/// Gets or sets the default formatting for all inlines.
/// </summary>
public InlineFormatting InlineFormatting { get; set; }
/// <summary>
/// An internal processing flag that determines if the rendered link
/// is inside a block or not to trigger extra handling.
/// </summary>
public bool LinkInsideBlock { get; set; }
/// <summary>
/// Gets or sets the next footnote while rendering links.
/// </summary>
public int NextFootnoteNumber { get; set; }
/// <summary>
/// Ensures there are two blank lines before an element.
/// </summary>
/// <returns></returns>
public GemtextRenderer EnsureTwoLines()
{
if (this.previousWasLine)
{
return this;
}
this.WriteLine();
this.WriteLine();
return this;
}
/// <summary>
/// A wrapper method to push the state of LinkInsideBlock while
/// performing an action.
/// </summary>
/// <param name="action">The action to perform.</param>
public void WhileLinkInsideBlock(Action action)
{
bool oldState = this.LinkInsideBlock;
this.LinkInsideBlock = true;
action();
this.LinkInsideBlock = oldState;
}
/// <summary>
/// Writes the lines of a <see cref="LeafBlock" />
/// </summary>
/// <param name="leafBlock">The leaf block.</param>
/// <param name="writeEndOfLines">if set to <c>true</c> write end of lines.</param>
/// <returns>This instance</returns>
public GemtextRenderer WriteLeafRawLines(
LeafBlock leafBlock,
bool writeEndOfLines)
{
// Make sure we have sane input.
if (leafBlock == null)
{
throw new ArgumentNullException(nameof(leafBlock));
}
// If we have nothing to write, then don't do anything. Even though
// Markdig says this can't be null, `leafBlock.Lines` may be null
// according to the comments.
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (leafBlock.Lines.Lines == null)
{
return this;
}
// Go through the block and write out each of the lines.
StringLineGroup lines = leafBlock.Lines;
StringLine[] slices = lines.Lines;
for (int i = 0; i < lines.Count; i++)
{
if (!writeEndOfLines && i > 0)
{
this.WriteLine();
}
this.Write(ref slices[i].Slice);
if (writeEndOfLines)
{
this.WriteLine();
}
}
return this;
}
}
}

View File

@ -0,0 +1,19 @@
namespace MfGames.Markdown.Gemtext.Renderers
{
/// <summary>
/// Describes the ways of formatting a HTML block.
/// </summary>
public enum HtmlBlockFormatting
{
/// <summary>
/// Indicates that HTML code blocks should just be removed.
/// </summary>
Remove,
/// <summary>
/// Indicates that HTML code blocks should be treated as blocks with
/// "html" as the type.
/// </summary>
CodeBlock,
}
}

View File

@ -0,0 +1,21 @@
namespace MfGames.Markdown.Gemtext.Renderers
{
/// <summary>
/// Describes the ways of formatting inline elements such as emphasis,
/// strong, and other elements.
/// </summary>
public enum InlineFormatting
{
/// <summary>
/// Indicates that the inline should be remove and only the text
/// rendered.
/// </summary>
Remove,
/// <summary>
/// Indicates that the inline should be left in place in a normalized
/// form (such as converting `_italics_` into `*italics*`).
/// </summary>
Normalize,
}
}