Skip to content

Commit

Permalink
Add infrastructure and diagnostics for unsupported directives (#57)
Browse files Browse the repository at this point in the history
Mpdreamz authored Nov 12, 2024
1 parent f880e6d commit 53fd0b2
Showing 39 changed files with 338 additions and 92 deletions.
14 changes: 12 additions & 2 deletions src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ public void TryComplete(Exception? exception = null)
_ctxSource.Cancel();
}

public ValueTask<bool> WaitToWrite() => _channel.Writer.WaitToWriteAsync();

public void Write(Diagnostic diagnostic)
{
var written = _channel.Writer.TryWrite(diagnostic);
@@ -84,10 +86,18 @@ public class DiagnosticsCollector(ILoggerFactory loggerFactory, IReadOnlyCollect

public async Task StartAsync(Cancel ctx)
{
await Channel.WaitToWrite();
while (!Channel.CancellationToken.IsCancellationRequested)
{
while (await Channel.Reader.WaitToReadAsync(Channel.CancellationToken))
Drain();
try
{
while (await Channel.Reader.WaitToReadAsync(Channel.CancellationToken))
Drain();
}
catch
{
//ignore
}
}
Drain();

Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ public static class ProcessorDiagnosticExtensions
{
public static void EmitError(this InlineProcessor processor, int line, int column, int length, string message)
{
var context = processor.GetContext();
if (context.SkipValidation) return;
var d = new Diagnostic
{
Severity = Severity.Error,
@@ -21,6 +23,53 @@ public static void EmitError(this InlineProcessor processor, int line, int colum
Message = message,
Length = length
};
processor.GetBuildContext().Collector.Channel.Write(d);
context.Build.Collector.Channel.Write(d);
}


public static void EmitWarning(this BlockProcessor processor, int line, int column, int length, string message)
{
var context = processor.GetContext();
if (context.SkipValidation) return;
var d = new Diagnostic
{
Severity = Severity.Warning,
File = processor.GetContext().Path.FullName,
Column = column,
Line = line,
Message = message,
Length = length
};
context.Build.Collector.Channel.Write(d);
}

public static void EmitError(this ParserContext context, int line, int column, int length, string message)
{
if (context.SkipValidation) return;
var d = new Diagnostic
{
Severity = Severity.Error,
File = context.Path.FullName,
Column = column,
Line = line,
Message = message,
Length = length
};
context.Build.Collector.Channel.Write(d);
}

public static void EmitWarning(this ParserContext context, int line, int column, int length, string message)
{
if (context.SkipValidation) return;
var d = new Diagnostic
{
Severity = Severity.Warning,
File = context.Path.FullName,
Column = column,
Line = line,
Message = message,
Length = length
};
context.Build.Collector.Channel.Write(d);
}
}
6 changes: 3 additions & 3 deletions src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ public string Title
}
}

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
Classes = Properties.GetValueOrDefault("class");
CrossReferenceName = Properties.GetValueOrDefault("name");
@@ -38,9 +38,9 @@ public class DropdownBlock(DirectiveBlockParser parser, Dictionary<string, strin
: AdmonitionBlock(parser, "admonition", properties)
{
// ReSharper disable once RedundantOverriddenMember
public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
base.FinalizeAndValidate();
base.FinalizeAndValidate(context);
Classes = $"dropdown {Classes}";
}
}
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/CardBlock.cs
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ public class CardBlock(DirectiveBlockParser parser, Dictionary<string, string> p

public string? Footer { get; set; }

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
Title = Arguments;
Link = Properties.GetValueOrDefault("link");
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/CodeBlock.cs
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ public string Language
}
}

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
Caption = Properties.GetValueOrDefault("caption");
CrossReferenceName = Properties.GetValueOrDefault("name");
3 changes: 2 additions & 1 deletion src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs
Original file line number Diff line number Diff line change
@@ -66,7 +66,8 @@ public abstract class DirectiveBlock(DirectiveBlockParser parser, Dictionary<str
/// <summary>
/// Allows blocks to finalize setting properties once fully parsed
/// </summary>
public abstract void FinalizeAndValidate();
/// <param name="context"></param>
public abstract void FinalizeAndValidate(ParserContext context);

protected void ParseBool(string key, Action<bool> setter)
{
21 changes: 20 additions & 1 deletion src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.

using System.Collections.Frozen;
using Markdig.Parsers;
using Markdig.Syntax;
using static System.StringSplitOptions;
@@ -37,6 +38,20 @@ public DirectiveBlockParser()

private readonly string[] _codeBlocks = [ "code", "code-block", "sourcecode"];

private readonly FrozenDictionary<string, int> _unsupportedBlocks = new Dictionary<string, int>
{
{ "bibliography", 5 },
{ "blockquote", 6 },
{ "csv-table", 9 },
{ "iframe", 14 },
{ "list-table", 17 },
{ "myst", 22 },
{ "topic", 24 },
{ "exercise", 30 },
{ "solution", 31 },
{ "toctree", 32 },
}.ToFrozenDictionary();

protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor)
{
_admonitionData = new Dictionary<string, string>();
@@ -108,14 +123,18 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor)
if (info.IndexOf($"{{{code}}}") > 0)
return new CodeBlock(this, code, _admonitionData);
}
// TODO alternate lookup .NET 9
var directive = info.ToString().Trim(['{', '}', '`']);
if (_unsupportedBlocks.TryGetValue(directive, out var issueId))
return new UnsupportedDirectiveBlock(this, directive, _admonitionData, issueId);

return new UnknownDirectiveBlock(this, info.ToString(), _admonitionData);
}

public override bool Close(BlockProcessor processor, Block block)
{
if (block is DirectiveBlock directiveBlock)
directiveBlock.FinalizeAndValidate();
directiveBlock.FinalizeAndValidate(processor.GetContext());


if (block is not TocTreeBlock toc)
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/ImageBlock.cs
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ public class ImageBlock(DirectiveBlockParser parser, Dictionary<string, string>

public string ImageUrl { get; private set; } = default!;

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
ImageUrl = Arguments ?? string.Empty; //todo validate
Classes = Properties.GetValueOrDefault("class");
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ public class IncludeBlock(DirectiveBlockParser parser, Dictionary<string, string

//TODO add all options from
//https://mystmd.org/guide/directives#directive-include
public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
var includePath = Arguments; //todo validate
Literal |= bool.TryParse(Properties.GetValueOrDefault("literal"), out var b) && b;
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/MermaidBlock.cs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ namespace Elastic.Markdown.Myst.Directives;
public class MermaidBlock(DirectiveBlockParser parser, Dictionary<string, string> properties)
: DirectiveBlock(parser, properties)
{
public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
}
}
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/SideBarBlock.cs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ namespace Elastic.Markdown.Myst.Directives;
public class SideBarBlock(DirectiveBlockParser parser, Dictionary<string, string> properties)
: DirectiveBlock(parser, properties)
{
public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
}
}
18 changes: 4 additions & 14 deletions src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ public class TabSetBlock(DirectiveBlockParser parser, Dictionary<string, string>
: DirectiveBlock(parser, properties)
{
public int Index { get; set; }
public override void FinalizeAndValidate() => Index = FindIndex();
public override void FinalizeAndValidate(ParserContext context) => Index = FindIndex();

private int _index = -1;
public int FindIndex()
@@ -28,7 +28,7 @@ public class TabItemBlock(DirectiveBlockParser parser, Dictionary<string, string
public int Index { get; set; }
public int TabSetIndex { get; set; }

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
Title = Arguments ?? "Unnamed Tab";
Index = Parent!.IndexOf(this);
@@ -80,7 +80,7 @@ public class GridBlock(DirectiveBlockParser parser, Dictionary<string, string> p
public string? ClassRow { get; set; }


public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
//todo we always assume 4 integers
if (!string.IsNullOrEmpty(Arguments))
@@ -124,17 +124,7 @@ private void ParseData(string data, Action<int, int, int, int> setter, bool allo
public class GridItemCardBlock(DirectiveBlockParser parser, Dictionary<string, string> properties)
: DirectiveBlock(parser, properties)
{
public override void FinalizeAndValidate()
{
}
}

public class UnknownDirectiveBlock(DirectiveBlockParser parser, string directive, Dictionary<string, string> properties)
: DirectiveBlock(parser, properties)
{
public string Directive => directive;

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
}
}
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/TocTreeBlock.cs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ public class TocTreeBlock(DirectiveBlockParser parser, Dictionary<string, string
{
public OrderedList<TocTreeLink> Links { get; } = new();

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
}
}
15 changes: 15 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/UnknownDirectiveBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elastic.Markdown.Myst.Directives;

public class UnknownDirectiveBlock(DirectiveBlockParser parser, string directive, Dictionary<string, string> properties)
: DirectiveBlock(parser, properties)
{
public string Directive => directive;

public override void FinalizeAndValidate(ParserContext context)
{
}
}
18 changes: 18 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/UnsupportedDirectiveBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using Elastic.Markdown.Diagnostics;

namespace Elastic.Markdown.Myst.Directives;

public class UnsupportedDirectiveBlock(DirectiveBlockParser parser, string directive, Dictionary<string, string> properties, int issueId)
: DirectiveBlock(parser, properties)
{
public string Directive => directive;

public string IssueUrl => $"https://github.com/elastic/docs-builder/issues/{issueId}";

public override void FinalizeAndValidate(ParserContext context) =>
context.EmitWarning(line:1, column:1, length:2, message: $"Directive block '{directive}' is unsupported. See {IssueUrl} for more information.");
}
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/Directives/VersionBlock.cs
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ public string Title
}
}

public override void FinalizeAndValidate()
public override void FinalizeAndValidate(ParserContext context)
{
}
}
19 changes: 13 additions & 6 deletions src/Elastic.Markdown/Myst/MarkdownParser.cs
Original file line number Diff line number Diff line change
@@ -36,30 +36,37 @@ public class MarkdownParser(IDirectoryInfo sourcePath, BuildContext context)
// TODO only scan for yaml front matter and toc information
public Task<MarkdownDocument> QuickParseAsync(IFileInfo path, Cancel ctx)
{
var context = new ParserContext(this, path, null, Context);
return ParseAsync(path, context, ctx);
var context = new ParserContext(this, path, null, Context)
{
SkipValidation = true
};
return ParseAsync(path, context, Pipeline, ctx);
}

public Task<MarkdownDocument> ParseAsync(IFileInfo path, YamlFrontMatter? matter, Cancel ctx)
{
var context = new ParserContext(this, path, matter, Context);
return ParseAsync(path, context, ctx);
return ParseAsync(path, context, Pipeline, ctx);
}

private async Task<MarkdownDocument> ParseAsync(IFileInfo path, MarkdownParserContext context, Cancel ctx)
private async Task<MarkdownDocument> ParseAsync(
IFileInfo path,
MarkdownParserContext context,
MarkdownPipeline pipeline,
Cancel ctx)
{
if (path.FileSystem is FileSystem)
{
//real IO optimize through UTF8 stream reader.
await using var streamReader = new Utf8StreamReader(path.FullName, fileOpenMode: FileOpenMode.Throughput);
var inputMarkdown = await streamReader.AsTextReader().ReadToEndAsync(ctx);
var markdownDocument = Markdig.Markdown.Parse(inputMarkdown, Pipeline, context);
var markdownDocument = Markdig.Markdown.Parse(inputMarkdown, pipeline, context);
return markdownDocument;
}
else
{
var inputMarkdown = await path.FileSystem.File.ReadAllTextAsync(path.FullName, ctx);
var markdownDocument = Markdig.Markdown.Parse(inputMarkdown, Pipeline, context);
var markdownDocument = Markdig.Markdown.Parse(inputMarkdown, pipeline, context);
return markdownDocument;
}
}
1 change: 1 addition & 0 deletions src/Elastic.Markdown/Myst/ParserContext.cs
Original file line number Diff line number Diff line change
@@ -48,4 +48,5 @@ public ParserContext(MarkdownParser markdownParser,
public IFileInfo Path { get; }
public YamlFrontMatter? FrontMatter { get; }
public BuildContext Build { get; }
public bool SkipValidation { get; init; }
}
27 changes: 14 additions & 13 deletions tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public abstract class AdmonitionTests(string directive) : DirectiveTest<AdmonitionBlock>(
public abstract class AdmonitionTests(ITestOutputHelper output, string directive) : DirectiveTest<AdmonitionBlock>(output,
$$"""
```{{{directive}}}
This is an attention block
@@ -22,53 +23,53 @@ A regular paragraph.
public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be(directive);
}

public class AttentionTests() : AdmonitionTests("attention")
public class AttentionTests(ITestOutputHelper output) : AdmonitionTests(output, "attention")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Attention");
}
public class CautionTests() : AdmonitionTests("caution")
public class CautionTests(ITestOutputHelper output) : AdmonitionTests(output, "caution")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Caution");
}
public class DangerTests() : AdmonitionTests("danger")
public class DangerTests(ITestOutputHelper output) : AdmonitionTests(output, "danger")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Danger");
}
public class ErrorTests() : AdmonitionTests("error")
public class ErrorTests(ITestOutputHelper output) : AdmonitionTests(output, "error")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Error");
}
public class HintTests() : AdmonitionTests("hint")
public class HintTests(ITestOutputHelper output) : AdmonitionTests(output, "hint")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Hint");
}
public class ImportantTests() : AdmonitionTests("important")
public class ImportantTests(ITestOutputHelper output) : AdmonitionTests(output, "important")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Important");
}
public class NoteTests() : AdmonitionTests("note")
public class NoteTests(ITestOutputHelper output) : AdmonitionTests(output, "note")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Note");
}
public class SeeAlsoTests() : AdmonitionTests("seealso")
public class SeeAlsoTests(ITestOutputHelper output) : AdmonitionTests(output, "seealso")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("See Also");
}
public class TipTests() : AdmonitionTests("tip")
public class TipTests(ITestOutputHelper output) : AdmonitionTests(output, "tip")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Tip");
}

public class NoteTitleTests() : DirectiveTest<AdmonitionBlock>(
public class NoteTitleTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
"""
```{note} This is my custom note
This is an attention block
@@ -84,7 +85,7 @@ A regular paragraph.
public void SetsCustomTitle() => Block!.Title.Should().Be("Note This is my custom note");
}

public class AdmonitionTitleTests() : DirectiveTest<AdmonitionBlock>(
public class AdmonitionTitleTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
"""
```{admonition} This is my custom note
This is an attention block
@@ -101,7 +102,7 @@ A regular paragraph.
}


public class DropdownTitleTests() : DirectiveTest<AdmonitionBlock>(
public class DropdownTitleTests(ITestOutputHelper output) : DirectiveTest<AdmonitionBlock>(output,
"""
```{dropdown} This is my custom dropdown
:open:
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/Directives/CardTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class CardTests() : DirectiveTest<CardBlock>(
public class CardTests(ITestOutputHelper output) : DirectiveTest<CardBlock>(output,
"""
```{card} Card title
Card content
@@ -19,7 +20,7 @@ Card content

}

public class LinkCardTests() : DirectiveTest<CardBlock>(
public class LinkCardTests(ITestOutputHelper output) : DirectiveTest<CardBlock>(output,
"""
```{card} Clickable Card
:link: https://elastic.co/docs
11 changes: 6 additions & 5 deletions tests/Elastic.Markdown.Tests/Directives/CodeTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public abstract class CodeBlockTests(string directive, string? language = null) : DirectiveTest<CodeBlock>(
public abstract class CodeBlockTests(ITestOutputHelper output, string directive, string? language = null) : DirectiveTest<CodeBlock>(output,
$$"""
```{{directive}} {{language}}
var x = 1;
@@ -22,25 +23,25 @@ A regular paragraph.
public void SetsCorrectDirectiveType() => Block!.Directive.Should().Be(language != null ? directive.Trim('{','}') : "raw");
}

public class CodeBlockDirectiveTests() : CodeBlockTests("{code-block}", "csharp")
public class CodeBlockDirectiveTests(ITestOutputHelper output) : CodeBlockTests(output, "{code-block}", "csharp")
{
[Fact]
public void SetsLanguage() => Block!.Language.Should().Be("csharp");
}

public class CodeTests() : CodeBlockTests("{code}", "python")
public class CodeTests(ITestOutputHelper output) : CodeBlockTests(output, "{code}", "python")
{
[Fact]
public void SetsLanguage() => Block!.Language.Should().Be("python");
}

public class SourceCodeTests() : CodeBlockTests("{sourcecode}", "java")
public class SourceCodeTests(ITestOutputHelper output) : CodeBlockTests(output, "{sourcecode}", "java")
{
[Fact]
public void SetsLanguage() => Block!.Language.Should().Be("java");
}

public class RawMarkdownCodeBlockTests() : CodeBlockTests("javascript")
public class RawMarkdownCodeBlockTests(ITestOutputHelper output) : CodeBlockTests(output, "javascript")
{
[Fact]
public void SetsLanguage() => Block!.Language.Should().Be("javascript");
32 changes: 27 additions & 5 deletions tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs
Original file line number Diff line number Diff line change
@@ -9,11 +9,13 @@
using FluentAssertions;
using JetBrains.Annotations;
using Markdig.Syntax;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public abstract class DirectiveTest<TDirective>([LanguageInjection("markdown")]string content) : DirectiveTest(content)
public abstract class DirectiveTest<TDirective>(ITestOutputHelper output, [LanguageInjection("markdown")]string content)
: DirectiveTest(output, content)
where TDirective : DirectiveBlock
{
protected TDirective? Block { get; private set; }
@@ -31,16 +33,28 @@ public override async Task InitializeAsync()
public void BlockIsNotNull() => Block.Should().NotBeNull();

}

public class TestDiagnosticsCollector(ILoggerFactory logger)
: DiagnosticsCollector(logger, [])
{
private readonly List<Diagnostic> _diagnostics = new();

public IReadOnlyCollection<Diagnostic> Diagnostics => _diagnostics;

protected override void HandleItem(Diagnostic diagnostic) => _diagnostics.Add(diagnostic);
}

public abstract class DirectiveTest : IAsyncLifetime
{
protected MarkdownFile File { get; }
protected string Html { get; private set; }
protected MarkdownDocument Document { get; private set; }
protected MockFileSystem FileSystem { get; }
protected TestDiagnosticsCollector Collector { get; }

protected DirectiveTest([LanguageInjection("markdown")]string content)
protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown")]string content)
{
var logger = NullLoggerFactory.Instance;
var logger = new TestLoggerFactory(output);
FileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "docs/source/index.md", new MockFileData(content) }
@@ -51,11 +65,12 @@ protected DirectiveTest([LanguageInjection("markdown")]string content)

var file = FileSystem.FileInfo.New("docs/source/index.md");
var root = FileSystem.DirectoryInfo.New(Paths.Root.FullName);
Collector = new TestDiagnosticsCollector(logger);
var context = new BuildContext
{
ReadFileSystem = FileSystem,
WriteFileSystem = FileSystem,
Collector = new DiagnosticsCollector(logger, [])
Collector = Collector
};
var parser = new MarkdownParser(root, context);

@@ -66,8 +81,15 @@ protected DirectiveTest([LanguageInjection("markdown")]string content)

public virtual async Task InitializeAsync()
{
var collectTask = Task.Run(async () => await Collector.StartAsync(default), default);

Document = await File.ParseFullAsync(default);
Html = await File.CreateHtmlAsync(File.YamlFrontMatter, default);
Collector.Channel.TryComplete();

await collectTask;
await Collector.Channel.Reader.Completion;
await Collector.StopAsync(default);
}

public Task DisposeAsync() => Task.CompletedTask;
3 changes: 2 additions & 1 deletion tests/Elastic.Markdown.Tests/Directives/GridTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class GridTests() : DirectiveTest<GridBlock>(
public class GridTests(ITestOutputHelper output) : DirectiveTest<GridBlock>(output,
"""
````{grid} 2 2 3 4
```{grid-item-card} Admonitions
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/Directives/ImageTests.cs
Original file line number Diff line number Diff line change
@@ -4,10 +4,11 @@
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class ImageBlockTests() : DirectiveTest<ImageBlock>(
public class ImageBlockTests(ITestOutputHelper output) : DirectiveTest<ImageBlock>(output,
"""
```{image} /_static/img/observability.png
:alt: Elasticsearch
@@ -28,7 +29,7 @@ public void ParsesBreakPoint()
}
}

public class FigureTests() : DirectiveTest<ImageBlock>(
public class FigureTests(ITestOutputHelper output) : DirectiveTest<ImageBlock>(output,
"""
```{figure} https://github.com/rowanc1/pics/blob/main/sunset.png?raw=true
:label: myFigure
3 changes: 2 additions & 1 deletion tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs
Original file line number Diff line number Diff line change
@@ -4,10 +4,11 @@
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class MermaidBlockTests() : DirectiveTest<MermaidBlock>(
public class MermaidBlockTests(ITestOutputHelper output) : DirectiveTest<MermaidBlock>(output,
"""
```{mermaid} /_static/img/observability.png
flowchart LR
3 changes: 2 additions & 1 deletion tests/Elastic.Markdown.Tests/Directives/SideBarTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class SideBarTests() : DirectiveTest<SideBarBlock>(
public class SideBarTests(ITestOutputHelper output) : DirectiveTest<SideBarBlock>(output,
"""
```{sidebar}
This code is very helpful.
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/Directives/TabTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class TabTests() : DirectiveTest<TabSetBlock>(
public class TabTests(ITestOutputHelper output) : DirectiveTest<TabSetBlock>(output,
"""
`````{tab-set}
@@ -54,7 +55,7 @@ public void ParsesTabItems()
}
}

public class MultipleTabTests() : DirectiveTest<TabSetBlock>(
public class MultipleTabTests(ITestOutputHelper output) : DirectiveTest<TabSetBlock>(output,
"""
`````{tab-set}
````{tab-item} Admonition
52 changes: 52 additions & 0 deletions tests/Elastic.Markdown.Tests/Directives/UnsupportedTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public abstract class UnsupportedDirectiveTests(ITestOutputHelper output, string directive)
: DirectiveTest<UnsupportedDirectiveBlock>(output,
$$"""
Content before bad directive
```{{{directive}}}
Version brief summary
```
A regular paragraph.
"""
)
{
[Fact]
public void ParsesAdmonitionBlock() => Block.Should().NotBeNull();

[Fact]
public void SetsCorrectDirectiveType() => Block!.Directive.Should().Be(directive);

[Fact]
public void TracksASingleWarning() => Collector.Warnings.Should().Be(1);

[Fact]
public void EmitsUnsupportedWarnings()
{
Collector.Diagnostics.Should().NotBeNullOrEmpty()
.And.HaveCount(1);
Collector.Diagnostics.Should().OnlyContain(d => d.Severity == Severity.Warning);
Collector.Diagnostics.Should()
.OnlyContain(d => d.Message.StartsWith($"Directive block '{directive}' is unsupported."));
}
}

public class BibliographyDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "bibliography");
public class BlockQuoteDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "blockquote");
public class FrameDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "iframe");
public class CsvTableDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "csv-table");
public class MystDirectiveDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "myst");
public class TopicDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "topic");
public class ExerciseDirectiveTest(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "exercise");
public class SolutionDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "solution");
public class TocTreeDirectiveTests(ITestOutputHelper output) : UnsupportedDirectiveTests(output, "solution");
12 changes: 7 additions & 5 deletions tests/Elastic.Markdown.Tests/Directives/VersionTests.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Markdig.Syntax;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public abstract class VersionTests(string directive) : DirectiveTest<VersionBlock>(
public abstract class VersionTests(ITestOutputHelper output, string directive) : DirectiveTest<VersionBlock>(output,
$$"""
```{{{directive}}}
Version brief summary
@@ -22,23 +24,23 @@ A regular paragraph.
public void SetsCorrectDirectiveType() => Block!.Directive.Should().Be(directive);
}

public class VersionAddedTests() : VersionTests("versionadded")
public class VersionAddedTests(ITestOutputHelper output) : VersionTests(output, "versionadded")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Version Added");
}

public class VersionChangedTests() : VersionTests("versionchanged")
public class VersionChangedTests(ITestOutputHelper output) : VersionTests(output, "versionchanged")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Version Changed");
}
public class VersionRemovedTests() : VersionTests("versionremoved")
public class VersionRemovedTests(ITestOutputHelper output) : VersionTests(output, "versionremoved")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Version Removed");
}
public class VersionDeprectatedTests() : VersionTests("deprecated")
public class VersionDeprectatedTests(ITestOutputHelper output) : VersionTests(output, "deprecated")
{
[Fact]
public void SetsTitle() => Block!.Title.Should().Be("Deprecated");
Original file line number Diff line number Diff line change
@@ -2,10 +2,11 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class YamlFrontMatterTests() : DirectiveTest(
public class YamlFrontMatterTests(ITestOutputHelper output) : DirectiveTest(output,
"""
---
title: Elastic Docs v3
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@
using Elastic.Markdown.Myst.Directives;
using Elastic.Markdown.Tests.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.FileInclusion;


public class IncludeTests() : DirectiveTest<IncludeBlock>(
public class IncludeTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
```{include} snippets/test.md
```
@@ -35,7 +36,7 @@ public void IncludesInclusionHtml() =>
}


public class IncludeSubstitutionTests() : DirectiveTest<IncludeBlock>(
public class IncludeSubstitutionTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
---
title: My Document
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@
using Elastic.Markdown.Myst.Directives;
using Elastic.Markdown.Tests.Directives;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.FileInclusion;


public class LiteralIncludeUsingPropertyTests() : DirectiveTest<IncludeBlock>(
public class LiteralIncludeUsingPropertyTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
```{include} snippets/test.txt
:literal: true
@@ -35,7 +36,7 @@ public void IncludesInclusionHtml() =>
}


public class LiteralIncludeTests() : DirectiveTest<IncludeBlock>(
public class LiteralIncludeTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
```{literalinclude} snippets/test.md
```
3 changes: 2 additions & 1 deletion tests/Elastic.Markdown.Tests/Inline/CommentTest.cs
Original file line number Diff line number Diff line change
@@ -2,10 +2,11 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Inline;

public class CommentTest() : InlineTest(
public class CommentTest(ITestOutputHelper output) : InlineTest(output,
"""
% comment
not a comment
3 changes: 2 additions & 1 deletion tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using FluentAssertions;
using Markdig.Syntax.Inlines;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Inline;

public class InlineImageTest() : InlineTest<LinkInline>(
public class InlineImageTest(ITestOutputHelper output) : InlineTest<LinkInline>(output,
"""
![Elasticsearch](/_static/img/observability.png){w=350px align=center}
"""
13 changes: 7 additions & 6 deletions tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs
Original file line number Diff line number Diff line change
@@ -5,16 +5,16 @@
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.IO;
using Elastic.Markdown.Myst;
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using JetBrains.Annotations;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Inline;

public abstract class LeafTest<TDirective>([LanguageInjection("markdown")]string content) : InlineTest(content)
public abstract class LeafTest<TDirective>(ITestOutputHelper output, [LanguageInjection("markdown")]string content)
: InlineTest(output, content)
where TDirective : LeafInline
{
protected TDirective? Block { get; private set; }
@@ -37,7 +37,8 @@ public override async Task InitializeAsync()

}

public abstract class InlineTest<TDirective>([LanguageInjection("markdown")]string content) : InlineTest(content)
public abstract class InlineTest<TDirective>(ITestOutputHelper output, [LanguageInjection("markdown")]string content)
: InlineTest(output, content)
where TDirective : ContainerInline
{
protected TDirective? Block { get; private set; }
@@ -65,9 +66,9 @@ public abstract class InlineTest : IAsyncLifetime
protected string Html { get; private set; }
protected MarkdownDocument Document { get; private set; }

protected InlineTest([LanguageInjection("markdown")]string content)
protected InlineTest(ITestOutputHelper output, [LanguageInjection("markdown")]string content)
{
var logger = NullLoggerFactory.Instance;
var logger = new TestLoggerFactory(output);
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "docs/source/index.md", new MockFileData(content) }
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Myst.Substitution;
using FluentAssertions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Inline;

public class SubstitutionTest() : LeafTest<SubstitutionLeaf>(
public class SubstitutionTest(ITestOutputHelper output) : LeafTest<SubstitutionLeaf>(output,
"""
---
sub:
@@ -30,7 +31,7 @@ public void GeneratesAttributesInHtml() =>
);
}

public class NeedsDoubleBrackets() : InlineTest(
public class NeedsDoubleBrackets(ITestOutputHelper output) : InlineTest(output,
"""
---
sub:
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs
Original file line number Diff line number Diff line change
@@ -6,15 +6,16 @@
using Elastic.Markdown.IO;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests;

public class OutputDirectoryTests
public class OutputDirectoryTests(ITestOutputHelper output)
{
[Fact]
public async Task CreatesDefaultOutputDirectory()
{
var logger = NullLoggerFactory.Instance;
var logger = new TestLoggerFactory(output);
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "docs/source/index.md", new MockFileData("test") }
5 changes: 3 additions & 2 deletions tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs
Original file line number Diff line number Diff line change
@@ -7,15 +7,16 @@
using Elastic.Markdown.IO;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.SiteMap;

public class NavigationTests
public class NavigationTests(ITestOutputHelper output)
{
[Fact]
public async Task CreatesDefaultOutputDirectory()
{
var logger = NullLoggerFactory.Instance;
var logger = new TestLoggerFactory(output);
var readFs = new FileSystem(); //use real IO to read docs.
var writeFs = new MockFileSystem(new MockFileSystemOptions //use in memory mock fs to test generation
{
43 changes: 43 additions & 0 deletions tests/Elastic.Markdown.Tests/TestLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using Microsoft.Extensions.Logging;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests;

public class TestLogger(ITestOutputHelper output) : ILogger
{
private class NullScope : IDisposable
{
public void Dispose() { }
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull => new NullScope();

public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Trace;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) =>
output.WriteLine(formatter(state, exception));
}

public class TestLoggerProvider(ITestOutputHelper output) : ILoggerProvider
{
public void Dispose()
{
}

public ILogger CreateLogger(string categoryName) => new TestLogger(output);
}

public class TestLoggerFactory(ITestOutputHelper output) : ILoggerFactory
{
public void Dispose()
{
}

public void AddProvider(ILoggerProvider provider) { }

public ILogger CreateLogger(string categoryName) => new TestLogger(output);
}

0 comments on commit 53fd0b2

Please sign in to comment.