feat: implemented wiki links for both HTML and Gemtext

This commit is contained in:
D. Moonfire 2022-11-02 17:37:04 -05:00
parent 45f26437ff
commit cd9c8989dc
47 changed files with 670 additions and 106 deletions

View file

@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown.Gemtext",
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown.Gemtext.Tests", "tests\MfGames.Markdown.Gemtext.Tests\MfGames.Markdown.Gemtext.Tests.csproj", "{D2703B25-9AF7-49FF-93A4-CB124560F2A9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown.Gemtext.Tests", "tests\MfGames.Markdown.Gemtext.Tests\MfGames.Markdown.Gemtext.Tests.csproj", "{D2703B25-9AF7-49FF-93A4-CB124560F2A9}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B4837F83-8560-4AC9-B1E3-6B8C0545A65B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown", "src\MfGames.Markdown\MfGames.Markdown.csproj", "{482E332F-9E72-4F02-B528-9CF04AE05E82}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Markdown.Tests", "tests\MfGames.Markdown.Tests\MfGames.Markdown.Tests.csproj", "{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -46,9 +52,35 @@ Global
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
{D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x86.Build.0 = Release|Any CPU {D2703B25-9AF7-49FF-93A4-CB124560F2A9}.Release|x86.Build.0 = Release|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Debug|x64.ActiveCfg = Debug|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Debug|x64.Build.0 = Debug|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Debug|x86.ActiveCfg = Debug|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Debug|x86.Build.0 = Debug|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Release|Any CPU.Build.0 = Release|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Release|x64.ActiveCfg = Release|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Release|x64.Build.0 = Release|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Release|x86.ActiveCfg = Release|Any CPU
{482E332F-9E72-4F02-B528-9CF04AE05E82}.Release|x86.Build.0 = Release|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Debug|x64.Build.0 = Debug|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Debug|x86.Build.0 = Debug|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Release|Any CPU.Build.0 = Release|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Release|x64.ActiveCfg = Release|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Release|x64.Build.0 = Release|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Release|x86.ActiveCfg = Release|Any CPU
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{2A1DE43D-544D-4CE0-ACC8-D15497BBCECA} = {954BA984-D50E-4D1C-880F-EAE678EF6945} {2A1DE43D-544D-4CE0-ACC8-D15497BBCECA} = {954BA984-D50E-4D1C-880F-EAE678EF6945}
{D2703B25-9AF7-49FF-93A4-CB124560F2A9} = {954BA984-D50E-4D1C-880F-EAE678EF6945} {D2703B25-9AF7-49FF-93A4-CB124560F2A9} = {B4837F83-8560-4AC9-B1E3-6B8C0545A65B}
{482E332F-9E72-4F02-B528-9CF04AE05E82} = {954BA984-D50E-4D1C-880F-EAE678EF6945}
{3E2AD98D-D1A1-48DF-B94F-A470982DAC9B} = {B4837F83-8560-4AC9-B1E3-6B8C0545A65B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View file

@ -1,3 +1,33 @@
# MfGames.Markdown.Gemtext CIL # MfGames.Markdown CIL
An extension for [Markdig](https://github.com/xoofx/markdig) that converts Markdown into Gemtext. This is a set of libraries for working with Markdown in C#. Most of the code are extensions in [MarkDig](https://github.com/xoofx/markdig), an extensible library for converting Markdown.
The library includes the following:
- An extension for converting wiki links, such as `[[MfGames]]` into a link based on the title. This is for both HTML and Gemtext.
- A output library for using MarkDig to generate Gemtext for Gemini pods.
The documentation is rather light at the moment, but the tests are set up to show these can be used.
## Usage
These library are not on nuget.org (for various reasons). To use them, set up your NuGet.config to pull them from their repository.
```
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="mfgames.com" value="https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json" protocolVersion="3" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="mfgames.com">
<package pattern="MfGames.*" />
</packageSource>
</packageSourceMapping>
</configuration>
```

View file

@ -1,7 +1,12 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Set up logging.
log() { echo "🛠️ $(basename $0): $@"; }
# Move into the top level.
cd $(dirname $0)/.. cd $(dirname $0)/..
./scripts/setup.sh || exit 1 ./scripts/setup.sh || exit 1
echo "$(basename $0): building project" log "building project"
dotnet build dotnet build

View file

@ -1,3 +0,0 @@
npm install --ci
dotnet restore
dotnet build

View file

@ -1,5 +0,0 @@
npm install --ci
dotnet restore
dotnet build
npm install --ci
npx semantic-release

View file

@ -1,9 +0,0 @@
npm install --ci
npx commitlint-gitlab-ci -x @commitlint/config-conventional
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"
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

View file

@ -1,39 +1,44 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Set up logging.
log() { echo "️🚢 $(basename $0): $@"; }
# Move into the root folder and make sure we're set up.
cd $(dirname $0)/.. cd $(dirname $0)/..
./scripts/setup.sh || exit 1 ./scripts/setup.sh || exit 1
# Verify the input. # Verify the input.
if [ "x$GITEA_TOKEN" = "x" ] if [ "x$GITEA_TOKEN" = "x" ]
then then
echo "the environment variable GITEA_TOKEN is not defined" log "the environment variable GITEA_TOKEN is not defined"
exit 1 exit 1
fi fi
# Clean up everything from the previous runs. # Clean up everything from the previous runs.
echo "$(basename $0): cleaning project" log "cleaning project"
dotnet clean dotnet clean
# Version the file based on the Git repository. # Version the file based on the Git repository.
echo "$(basename $0): setting project version" log "setting project version"
(cd src && dotnet dotnet-gitversion /updateprojectfiles) (cd src && dotnet dotnet-gitversion /updateprojectfiles)
SEMVER="v$(dotnet gitversion /output json | jq -r .SemVer)" SEMVER="v$(dotnet gitversion /output json | jq -r .SemVer)"
if [ "x$SEMVER" = "x" ] if [ "x$SEMVER" = "x" ]
then then
echo "$(basename $0): cannot figure out the semantic version" log "cannot figure out the semantic version"
exit 1 exit 1
fi fi
# Build to pick up the new version. # Build to pick up the new version.
echo "$(basename $0): building project $SEMVER" log "building project $SEMVER"
dotnet build || exit 1 dotnet build || exit 1
# Create and publish the NuGet packages. # Create and publish the NuGet packages.
echo "$(basename $0): creating NuGet packages" log "creating NuGet packages"
dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1 dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
echo "$(basename $0): publishing NuGet package" log "publishing NuGet package"
dotnet nuget remove source mfgames.com >& /dev/null dotnet nuget remove source mfgames.com >& /dev/null
dotnet nuget add source --name mfgames.com --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1 dotnet nuget add source --name mfgames.com --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
dotnet nuget push --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1 dotnet nuget push --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1
@ -41,11 +46,11 @@ dotnet nuget push --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg
# Tag and push, but only if we don't have a tag. # Tag and push, but only if we don't have a tag.
if ! git tag | grep $SEMVER >& /dev/null if ! git tag | grep $SEMVER >& /dev/null
then then
echo "$(basename $0): tagging and pushing" log "tagging and pushing"
git remote add publish https://dmoonfire:$GITEA_TOKEN@src.mfgames.com/mfgames-cil/$(basename $(git config --get remote.origin.url)) git remote add publish https://dmoonfire:$GITEA_TOKEN@src.mfgames.com/mfgames-cil/$(basename $(git config --get remote.origin.url))
git tag $SEMVER git tag $SEMVER
git push publish $SEMVER || exit 1 git push publish $SEMVER || exit 1
git remote remove publish git remote remove publish
else else
echo "$(basename $0): not tagging, already exists" log "not tagging, already exists"
fi fi

View file

@ -1,5 +1,8 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Set up logging.
log() { echo "📦️ $(basename $0): $@";
# Normalize our environment. # Normalize our environment.
cd $(dirname $0)/.. cd $(dirname $0)/..
@ -8,7 +11,7 @@ for e in dotnet lefthook prettier nixfmt
do do
if ! which $e >& /dev/null if ! which $e >& /dev/null
then then
echo "Cannot find '$e' in the path" log "Cannot find '$e' in the path"
exit 1 exit 1
fi fi
done done
@ -16,12 +19,12 @@ done
# Make sure we have lefthook is installed. # Make sure we have lefthook is installed.
if [ ! -f .git/hooks/pre-commit ] if [ ! -f .git/hooks/pre-commit ]
then then
echo "$(basename $0): installing lefthook" log "installing lefthook"
lefthook install lefthook install
fi fi
# Make sure our tools are installed. # Make sure our tools are installed.
echo "$(basename $0): install .NET tools" log "install .NET tools"
dotnet tool restore dotnet tool restore
# Everything is good. # Everything is good.

View file

@ -2,6 +2,7 @@ using Markdig;
using Markdig.Extensions.SmartyPants; using Markdig.Extensions.SmartyPants;
using Markdig.Parsers.Inlines; using Markdig.Parsers.Inlines;
using Markdig.Renderers; using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines; using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
@ -44,7 +45,7 @@ namespace MfGames.Markdown.Gemtext.Extensions
} }
if (!gemtextRenderer.ObjectRenderers if (!gemtextRenderer.ObjectRenderers
.Contains<GemtextSmartyPantRenderer>()) .Contains<GemtextSmartyPantRenderer>())
{ {
gemtextRenderer.ObjectRenderers.Add( gemtextRenderer.ObjectRenderers.Add(
new GemtextSmartyPantRenderer(this.Options)); new GemtextSmartyPantRenderer(this.Options));

View file

@ -1,5 +1,6 @@
using Markdig; using Markdig;
using Markdig.Renderers; using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions namespace MfGames.Markdown.Gemtext.Extensions

View file

@ -1,5 +1,6 @@
using Markdig; using Markdig;
using Markdig.Renderers; using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks; using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
@ -26,7 +27,8 @@ namespace MfGames.Markdown.Gemtext.Extensions
return; return;
} }
var heading = gemtext.ObjectRenderers.Find<HeadingRenderer>(); HeadingRenderer? heading =
gemtext.ObjectRenderers.Find<HeadingRenderer>();
if (heading != null) if (heading != null)
{ {

View file

@ -1,5 +1,6 @@
using Markdig; using Markdig;
using Markdig.Renderers; using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions namespace MfGames.Markdown.Gemtext.Extensions

View file

@ -1,5 +1,6 @@
using Markdig; using Markdig;
using Markdig.Renderers; using Markdig.Renderers;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext.Extensions namespace MfGames.Markdown.Gemtext.Extensions

View file

@ -1,8 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using Markdig; using Markdig;
using Markdig.Parsers; using Markdig.Parsers;
using Markdig.Syntax; using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
namespace MfGames.Markdown.Gemtext namespace MfGames.Markdown.Gemtext

View file

@ -1,25 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dylan Moonfire</Authors> <Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company> <Company>Moonfire Games</Company>
<RepositoryUrl>https://gitlab.com/mfgames-cil/mfgames-markdown-gemtext-cil</RepositoryUrl> <RepositoryUrl>https://gitlab.com/mfgames-cil/mfgames-markdown-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<PackageTags>ecs</PackageTags> <PackageProjectUrl>https://gitlab.com/mfgames-cil/mfgames-markdown-cil</PackageProjectUrl>
<PackageProjectUrl>https://gitlab.com/mfgames-cil/mfgames-markdown-gemtext-cil</PackageProjectUrl> <PackageLicense>MIT</PackageLicense>
<PackageLicense>MIT</PackageLicense> <Description>A MarkDig extension to render Markdown in Gemtext.</Description>
<Description>A Markdown extension to render Markdown in Gemtext.</Description> </PropertyGroup>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ConsoleTableExt" Version="3.2.0" /> <PackageReference Include="ConsoleTableExt" Version="3.2.0"/>
<PackageReference Include="Markdig" Version="0.30.3" /> <PackageReference Include="Markdig" Version="0.30.3"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -16,7 +16,7 @@ namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks
renderer.EnsureTwoLines(); renderer.EnsureTwoLines();
// Go through each list item and write them out. // Go through each list item and write them out.
foreach (var item in listBlock) foreach (Block? item in listBlock)
{ {
// If the list only contains a link, then we just render the // If the list only contains a link, then we just render the
// link instead. // link instead.

View file

@ -1,4 +1,5 @@
using Markdig.Syntax; using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines; using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks

View file

@ -1,4 +1,5 @@
using Markdig.Syntax; using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines; using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Net; using System.Net;
using Markdig.Extensions.SmartyPants; using Markdig.Extensions.SmartyPants;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines

View file

@ -1,4 +1,5 @@
using System.IO; using System.IO;
using Markdig.Syntax.Inlines; using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
@ -25,7 +26,7 @@ namespace MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines
// write out each link which is already formatted. // write out each link which is already formatted.
renderer.WriteLine(); renderer.WriteLine();
foreach (var link in renderer.GatheredLinks) foreach (string? link in renderer.GatheredLinks)
{ {
renderer.WriteLine(); renderer.WriteLine();
renderer.Write(link); renderer.Write(link);

View file

@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using Markdig.Syntax; using Markdig.Syntax;
using Markdig.Syntax.Inlines; using Markdig.Syntax.Inlines;

View file

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Markdig.Helpers; using Markdig.Helpers;
using Markdig.Renderers; using Markdig.Renderers;
using Markdig.Syntax; using Markdig.Syntax;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks; using MfGames.Markdown.Gemtext.Renderers.Gemtext.Blocks;
using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines; using MfGames.Markdown.Gemtext.Renderers.Gemtext.Inlines;

View file

@ -0,0 +1,11 @@
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Extensions;
public class WikiLink : LinkInline
{
public WikiLink()
{
this.IsClosed = false;
}
}

View file

@ -0,0 +1,47 @@
using Markdig;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
namespace MfGames.Markdown.Extensions;
/// <summary>
/// Translate `[[Bob]]` into `/bob/`.
/// </summary>
public class WikiLinkExtension : IMarkdownExtension
{
public WikiLinkExtension()
: this(null)
{
}
public WikiLinkExtension(WikiLinkOptions? options)
{
this.Options = options ?? new WikiLinkOptions();
}
public WikiLinkOptions Options { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
WikiLinkInlineParser? parser = pipeline.InlineParsers
.FindExact<WikiLinkInlineParser>();
if (parser != null)
{
return;
}
parser = new WikiLinkInlineParser(this.Options);
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(parser);
}
/// <inheritdoc />
public void Setup(
MarkdownPipeline pipeline,
IMarkdownRenderer renderer)
{
// No setup needed here because we're using LinkInline which does the
// bulk of the work.
}
}

View file

@ -0,0 +1,112 @@
using System.Linq;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax.Inlines;
namespace MfGames.Markdown.Extensions;
public class WikiLinkInlineParser : InlineParser
{
private readonly WikiLinkOptions options;
public WikiLinkInlineParser(WikiLinkOptions options)
{
this.options = options;
this.OpeningCharacters = new[] { '[' };
}
/// <inheritdoc />
public override bool Match(
InlineProcessor processor,
ref StringSlice slice)
{
// We are looking for the `[[` opening for the tag and that the first
// one isn't escaped.
if (IsNotDelimiter(slice, '['))
{
return false;
}
// We need to loop over the entire link, including the `[[` and `]]`
// while keeping track since we'll swallow additional characters beyond
// the link.
int linkStart = slice.Start;
int linkEnd = slice.Start;
slice.Start += 2;
// Our content starts after the double '[['.
int contentStart = slice.Start;
// We need to find the end of the link (the `]]`).
while (IsNotDelimiter(slice, ']'))
{
slice.NextChar();
linkEnd = slice.Start;
}
// Pull out the components before we adjust for the ']]' for the end.
int contentEnd = linkEnd;
// Finish skipping over the `]]`.
slice.NextChar();
slice.NextChar();
// Format the label and the URL.
string content = slice.Text.Substring(
contentStart,
contentEnd - contentStart);
string[] contentParts = content.Split('|', 2);
string label = contentParts.Last();
string url = this.options.GetUrl(contentParts.First());
// Add in any trailing components. This merges the `'s` from
// `[[Dale]]'s` into the label.
while (this.options.IsTrailingLink(slice.CurrentChar))
{
label += slice.CurrentChar;
slice.NextChar();
linkEnd++;
}
// Create the link that we're replacing.
WikiLink link = new()
{
Span =
{
Start = processor.GetSourcePosition(
linkStart,
out int line,
out int column),
},
Line = line,
Column = column,
Url = url,
IsClosed = true,
};
link.AppendChild(
new LiteralInline()
{
Line = line,
Column = column,
Content = new StringSlice(label),
IsClosed = true,
});
// Replace the inline and then indicate we have a match.
processor.Inline = link;
return true;
}
private static bool IsNotDelimiter(
StringSlice slice,
char delimiter)
{
return slice.CurrentChar != delimiter
|| slice.PeekChar() != delimiter
|| slice.PeekCharExtra(-1) == '\\';
}
}

View file

@ -0,0 +1,31 @@
using System;
using Markdig.Helpers;
namespace MfGames.Markdown.Extensions;
public class WikiLinkOptions
{
public WikiLinkOptions()
{
this.GetUrl = a => a;
this.IsTrailingLink = a => a.IsAlpha() || a == '\'';
}
/// <summary>
/// The callback to determine the link from the given wiki link. This does
/// not include trailing additions or use the label (e.g., `(ab|cd)` would
/// get `ab` as the parameters of this function.
/// </summary>
public Func<string, string> GetUrl { get; set; }
/// <summary>
/// <para>
/// A callback to determine if the text after the link should be merged
/// with the link label. This allows links such as [[Dale]]'s to be turned
/// into "Dale's" but pointing to "Dale" as a page.
/// </para>
/// <para>The default is to include any character or the apostrophe.</para>
/// </summary>
public Func<char, bool> IsTrailingLink { get; set; }
}

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://gitlab.com/mfgames-cil/mfgames-markdown-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageProjectUrl>https://gitlab.com/mfgames-cil/mfgames-markdown-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
<Description>Various extensions for MarkDig and classes for working with Markdown.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.30.3"/>
</ItemGroup>
</Project>

View file

@ -1,10 +1,11 @@
using Markdig; using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class CodeInlineTests public class CodeInlineTests
{ {

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class HeaderTests public class HeaderTests
{ {

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class ImageLinkTests public class ImageLinkTests
{ {

View file

@ -1,9 +1,10 @@
using Markdig; using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class IncreaseHeaderDepthsAfterFirstTests public class IncreaseHeaderDepthsAfterFirstTests
{ {

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class LinkTests public class LinkTests
{ {

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class ListTests public class ListTests
{ {

View file

@ -1,26 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>MfGames.Markdown.Gemini.Tests</RootNamespace> </PropertyGroup>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" /> <PackageReference Include="MfGames.TestSetup" Version="1.0.6"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit" Version="2.4.2"/>
<PrivateAssets>all</PrivateAssets> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PackageReference Include="coverlet.collector" Version="3.1.2"> </PackageReference>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> <PrivateAssets>all</PrivateAssets>
</ItemGroup> </PackageReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Markdown.Gemtext\MfGames.Markdown.Gemtext.csproj" /> <ProjectReference Include="..\..\src\MfGames.Markdown.Gemtext\MfGames.Markdown.Gemtext.csproj"/>
</ItemGroup> <ProjectReference Include="..\..\src\MfGames.Markdown\MfGames.Markdown.csproj"/>
</ItemGroup>
</Project> </Project>

View file

@ -1,10 +1,11 @@
using Markdig; using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
/// <summary> /// <summary>
/// Tests the various functionality of the ParagraphLinkHandling and /// Tests the various functionality of the ParagraphLinkHandling and

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class PlainTextTests public class PlainTextTests
{ {

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired namespace MfGames.Markdown.Gemtext.Tests.PythonInspired
{ {
/// <summary> /// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_base_url.py /// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_base_url.py

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired namespace MfGames.Markdown.Gemtext.Tests.PythonInspired
{ {
/// <summary> /// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_codeblock.py /// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_codeblock.py

View file

@ -1,7 +1,6 @@
using MfGames.Markdown.Gemtext;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired namespace MfGames.Markdown.Gemtext.Tests.PythonInspired
{ {
/// <summary> /// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_list.py /// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_list.py

View file

@ -1,9 +1,10 @@
using Markdig; using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired namespace MfGames.Markdown.Gemtext.Tests.PythonInspired
{ {
/// <summary> /// <summary>
/// Runs through various plain input tests. /// Runs through various plain input tests.

View file

@ -1,12 +1,11 @@
using Markdig; using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers; using MfGames.Markdown.Gemtext.Renderers;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests.PythonInspired namespace MfGames.Markdown.Gemtext.Tests.PythonInspired
{ {
/// <summary> /// <summary>
/// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_strip_html.py /// https://github.com/makeworld-the-better-one/md2gemini/blob/master/tests/test_strip_html.py

View file

@ -1,10 +1,11 @@
using Markdig; using Markdig;
using Markdig.Extensions.SmartyPants; using Markdig.Extensions.SmartyPants;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class QuoteTests public class QuoteTests
{ {

View file

@ -2,12 +2,11 @@ using ConsoleTableExt;
using Markdig; using Markdig;
using MfGames.Markdown.Gemtext;
using MfGames.Markdown.Gemtext.Extensions; using MfGames.Markdown.Gemtext.Extensions;
using Xunit; using Xunit;
namespace MfGames.Markdown.Gemini.Tests namespace MfGames.Markdown.Gemtext.Tests
{ {
public class TableTests public class TableTests
{ {

View file

@ -0,0 +1,56 @@
using Markdig;
using MfGames.Markdown.Extensions;
using MfGames.Markdown.Gemtext.Extensions;
using MfGames.Markdown.Gemtext.Renderers;
using MfGames.TestSetup;
using Xunit;
using Xunit.Abstractions;
namespace MfGames.Markdown.Gemtext.Tests;
/// <summary>
/// Tests the functionality of the WriteFiles().
/// </summary>
public class WikiLinkTest : TestBase<TestContext>
{
public WikiLinkTest(ITestOutputHelper output)
: base(output)
{
}
[Fact]
public void PossessiveLabeledWikiLinkGemtext()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = MarkdownGemtext
.ToGemtext(
"[[Test|Label]]'s",
pipeline)
.Replace("%5E", "^");
Assert.Equal("=> ^test$ Label's", result);
}
private static MarkdownPipeline CreatePipeline()
{
// Convert all titles to slugs.
WikiLinkOptions options = new()
{
GetUrl = (title) => string.Format(
"^{0}$",
title.ToLowerInvariant().Replace(" ", "-")),
};
// Create the pipeline and return the results.
MarkdownPipelineBuilder builder = new MarkdownPipelineBuilder()
.Use(new WikiLinkExtension(options))
.Use(new SetBlockLinkHandling(BlockLinkHandling.DocumentEnd));
// Build the pipeline and return it.
MarkdownPipeline pipeline = builder.Build();
return pipeline;
}
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.30.3"/>
<PackageReference Include="MfGames.TestSetup" Version="1.0.6"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Markdown\MfGames.Markdown.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,188 @@
using Markdig;
using MfGames.Markdown.Extensions;
using MfGames.TestSetup;
using Xunit;
using Xunit.Abstractions;
namespace MfGames.Markdown.Tests;
/// <summary>
/// Tests the functionality of the WriteFiles().
/// </summary>
public class WikiLinkTest : TestBase<TestContext>
{
public WikiLinkTest(ITestOutputHelper output)
: base(output)
{
}
[Fact]
public void BareStringHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown.ToHtml(
"This is text.",
pipeline);
Assert.Equal("<p>This is text.</p>\n", result);
}
[Fact]
public void BlankStringHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown.ToHtml(
"",
pipeline);
Assert.Equal("", result);
}
[Fact]
public void LabeledWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test|Label]]",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Label</a></p>\n", result);
}
[Fact]
public void PossessiveLabeledWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test|Label]]'s",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Label's</a></p>\n", result);
}
[Fact]
public void PossessiveWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test]]'s",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Test's</a></p>\n", result);
}
[Fact]
public void PunctuationLabeledWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test|Label]].",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Label</a>.</p>\n", result);
}
[Fact]
public void PunctuationWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test]].",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Test</a>.</p>\n", result);
}
[Fact]
public void SentenceWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"This is a [[test]] of this system.",
pipeline)
.Replace("%5E", "^");
Assert.Equal(
"<p>This is a <a href=\"^test$\">test</a> of this system.</p>\n",
result);
}
[Fact]
public void SimpleWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test]]",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Test</a></p>\n", result);
}
[Fact]
public void StandardLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown.ToHtml(
"[Test](https://test/)",
pipeline);
Assert.Equal("<p><a href=\"https://test/\">Test</a></p>\n", result);
}
[Fact]
public void TrailingLabeledWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test|Label]]s",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Labels</a></p>\n", result);
}
[Fact]
public void TrailingWikiLinkHtml()
{
MarkdownPipeline pipeline = CreatePipeline();
string result = Markdig.Markdown
.ToHtml(
"[[Test]]s",
pipeline)
.Replace("%5E", "^");
Assert.Equal("<p><a href=\"^test$\">Tests</a></p>\n", result);
}
private static MarkdownPipeline CreatePipeline()
{
WikiLinkOptions options = new()
{
GetUrl = (title) => string.Format(
"^{0}$",
title.ToLowerInvariant().Replace(" ", "-")),
};
MarkdownPipelineBuilder? builder = new MarkdownPipelineBuilder()
.Use(new WikiLinkExtension(options));
MarkdownPipeline? pipeline = builder.Build();
return pipeline;
}
}