diff --git a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs index dde72b87..d69fc7a6 100644 --- a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs +++ b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs @@ -29,6 +29,8 @@ public void TryComplete(Exception? exception = null) _ctxSource.Cancel(); } + public ValueTask 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(); diff --git a/src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs b/src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs index 5da54ccc..bf671472 100644 --- a/src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs +++ b/src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs @@ -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); } } diff --git a/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs b/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs index 02b3a768..b622eda4 100644 --- a/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs @@ -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 p public string? Footer { get; set; } - public override void FinalizeAndValidate() + public override void FinalizeAndValidate(ParserContext context) { Title = Arguments; Link = Properties.GetValueOrDefault("link"); diff --git a/src/Elastic.Markdown/Myst/Directives/CodeBlock.cs b/src/Elastic.Markdown/Myst/Directives/CodeBlock.cs index 176e4b9d..515870cf 100644 --- a/src/Elastic.Markdown/Myst/Directives/CodeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/CodeBlock.cs @@ -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"); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs index 28734c58..5a2cb6d2 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs @@ -66,7 +66,8 @@ public abstract class DirectiveBlock(DirectiveBlockParser parser, Dictionary /// Allows blocks to finalize setting properties once fully parsed /// - public abstract void FinalizeAndValidate(); + /// + public abstract void FinalizeAndValidate(ParserContext context); protected void ParseBool(string key, Action setter) { diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs index 2db24cca..e68a069e 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs @@ -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 _unsupportedBlocks = new Dictionary + { + { "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(); @@ -108,6 +123,10 @@ 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); } @@ -115,7 +134,7 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor) 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) diff --git a/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs b/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs index 7a56624b..96e28b02 100644 --- a/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs @@ -56,7 +56,7 @@ public class ImageBlock(DirectiveBlockParser parser, Dictionary 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"); diff --git a/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs index f22c34f3..8e4a18ff 100644 --- a/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs @@ -28,7 +28,7 @@ public class IncludeBlock(DirectiveBlockParser parser, Dictionary properties) : DirectiveBlock(parser, properties) { - public override void FinalizeAndValidate() + public override void FinalizeAndValidate(ParserContext context) { } } diff --git a/src/Elastic.Markdown/Myst/Directives/SideBarBlock.cs b/src/Elastic.Markdown/Myst/Directives/SideBarBlock.cs index 580fa357..0c1bd8fa 100644 --- a/src/Elastic.Markdown/Myst/Directives/SideBarBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/SideBarBlock.cs @@ -6,7 +6,7 @@ namespace Elastic.Markdown.Myst.Directives; public class SideBarBlock(DirectiveBlockParser parser, Dictionary properties) : DirectiveBlock(parser, properties) { - public override void FinalizeAndValidate() + public override void FinalizeAndValidate(ParserContext context) { } } diff --git a/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs b/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs index 33ce7ae7..08532730 100644 --- a/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs @@ -10,7 +10,7 @@ public class TabSetBlock(DirectiveBlockParser parser, Dictionary : 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 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 setter, bool allo public class GridItemCardBlock(DirectiveBlockParser parser, Dictionary properties) : DirectiveBlock(parser, properties) { - public override void FinalizeAndValidate() - { - } -} - -public class UnknownDirectiveBlock(DirectiveBlockParser parser, string directive, Dictionary properties) - : DirectiveBlock(parser, properties) -{ - public string Directive => directive; - - public override void FinalizeAndValidate() + public override void FinalizeAndValidate(ParserContext context) { } } diff --git a/src/Elastic.Markdown/Myst/Directives/TocTreeBlock.cs b/src/Elastic.Markdown/Myst/Directives/TocTreeBlock.cs index 84813033..ef725548 100644 --- a/src/Elastic.Markdown/Myst/Directives/TocTreeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/TocTreeBlock.cs @@ -16,7 +16,7 @@ public class TocTreeBlock(DirectiveBlockParser parser, Dictionary Links { get; } = new(); - public override void FinalizeAndValidate() + public override void FinalizeAndValidate(ParserContext context) { } } diff --git a/src/Elastic.Markdown/Myst/Directives/UnknownDirectiveBlock.cs b/src/Elastic.Markdown/Myst/Directives/UnknownDirectiveBlock.cs new file mode 100644 index 00000000..050190be --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/UnknownDirectiveBlock.cs @@ -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 properties) + : DirectiveBlock(parser, properties) +{ + public string Directive => directive; + + public override void FinalizeAndValidate(ParserContext context) + { + } +} diff --git a/src/Elastic.Markdown/Myst/Directives/UnsupportedDirectiveBlock.cs b/src/Elastic.Markdown/Myst/Directives/UnsupportedDirectiveBlock.cs new file mode 100644 index 00000000..6576c721 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/UnsupportedDirectiveBlock.cs @@ -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 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."); +} diff --git a/src/Elastic.Markdown/Myst/Directives/VersionBlock.cs b/src/Elastic.Markdown/Myst/Directives/VersionBlock.cs index e7b674f2..4429098e 100644 --- a/src/Elastic.Markdown/Myst/Directives/VersionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/VersionBlock.cs @@ -21,7 +21,7 @@ public string Title } } - public override void FinalizeAndValidate() + public override void FinalizeAndValidate(ParserContext context) { } } diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 5376f41f..f4456ace 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -36,30 +36,37 @@ public class MarkdownParser(IDirectoryInfo sourcePath, BuildContext context) // TODO only scan for yaml front matter and toc information public Task 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 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 ParseAsync(IFileInfo path, MarkdownParserContext context, Cancel ctx) + private async Task 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; } } diff --git a/src/Elastic.Markdown/Myst/ParserContext.cs b/src/Elastic.Markdown/Myst/ParserContext.cs index 6eb9f99e..1949194c 100644 --- a/src/Elastic.Markdown/Myst/ParserContext.cs +++ b/src/Elastic.Markdown/Myst/ParserContext.cs @@ -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; } } diff --git a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs index b59fad62..913c7c5b 100644 --- a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs @@ -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( +public abstract class AdmonitionTests(ITestOutputHelper output, string directive) : DirectiveTest(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( +public class NoteTitleTests(ITestOutputHelper output) : DirectiveTest(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( +public class AdmonitionTitleTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{admonition} This is my custom note This is an attention block @@ -101,7 +102,7 @@ A regular paragraph. } -public class DropdownTitleTests() : DirectiveTest( +public class DropdownTitleTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{dropdown} This is my custom dropdown :open: diff --git a/tests/Elastic.Markdown.Tests/Directives/CardTests.cs b/tests/Elastic.Markdown.Tests/Directives/CardTests.cs index 53465134..50421dec 100644 --- a/tests/Elastic.Markdown.Tests/Directives/CardTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/CardTests.cs @@ -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( +public class CardTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{card} Card title Card content @@ -19,7 +20,7 @@ Card content } -public class LinkCardTests() : DirectiveTest( +public class LinkCardTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{card} Clickable Card :link: https://elastic.co/docs diff --git a/tests/Elastic.Markdown.Tests/Directives/CodeTests.cs b/tests/Elastic.Markdown.Tests/Directives/CodeTests.cs index c2e97f39..3973ae83 100644 --- a/tests/Elastic.Markdown.Tests/Directives/CodeTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/CodeTests.cs @@ -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( +public abstract class CodeBlockTests(ITestOutputHelper output, string directive, string? language = null) : DirectiveTest(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"); diff --git a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs index b05dea04..187967dd 100644 --- a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs @@ -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([LanguageInjection("markdown")]string content) : DirectiveTest(content) +public abstract class DirectiveTest(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 _diagnostics = new(); + + public IReadOnlyCollection 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 { { "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; diff --git a/tests/Elastic.Markdown.Tests/Directives/GridTests.cs b/tests/Elastic.Markdown.Tests/Directives/GridTests.cs index 77f8734e..54735429 100644 --- a/tests/Elastic.Markdown.Tests/Directives/GridTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/GridTests.cs @@ -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( +public class GridTests(ITestOutputHelper output) : DirectiveTest(output, """ ````{grid} 2 2 3 4 ```{grid-item-card} Admonitions diff --git a/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs b/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs index f9dd57ed..1f878b4e 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ImageTests.cs @@ -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( +public class ImageBlockTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{image} /_static/img/observability.png :alt: Elasticsearch @@ -28,7 +29,7 @@ public void ParsesBreakPoint() } } -public class FigureTests() : DirectiveTest( +public class FigureTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{figure} https://github.com/rowanc1/pics/blob/main/sunset.png?raw=true :label: myFigure diff --git a/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs b/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs index b679b5f4..b4d2dd61 100644 --- a/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/MermaidTests.cs @@ -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( +public class MermaidBlockTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{mermaid} /_static/img/observability.png flowchart LR diff --git a/tests/Elastic.Markdown.Tests/Directives/SideBarTests.cs b/tests/Elastic.Markdown.Tests/Directives/SideBarTests.cs index 24ebcf32..1e1b873f 100644 --- a/tests/Elastic.Markdown.Tests/Directives/SideBarTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/SideBarTests.cs @@ -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( +public class SideBarTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{sidebar} This code is very helpful. diff --git a/tests/Elastic.Markdown.Tests/Directives/TabTests.cs b/tests/Elastic.Markdown.Tests/Directives/TabTests.cs index a467e0f1..a36b85f0 100644 --- a/tests/Elastic.Markdown.Tests/Directives/TabTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/TabTests.cs @@ -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( +public class TabTests(ITestOutputHelper output) : DirectiveTest(output, """ `````{tab-set} @@ -54,7 +55,7 @@ public void ParsesTabItems() } } -public class MultipleTabTests() : DirectiveTest( +public class MultipleTabTests(ITestOutputHelper output) : DirectiveTest(output, """ `````{tab-set} ````{tab-item} Admonition diff --git a/tests/Elastic.Markdown.Tests/Directives/UnsupportedTests.cs b/tests/Elastic.Markdown.Tests/Directives/UnsupportedTests.cs new file mode 100644 index 00000000..a5500f84 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/Directives/UnsupportedTests.cs @@ -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(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"); diff --git a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs index 989230d0..5657365d 100644 --- a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs @@ -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( +public abstract class VersionTests(ITestOutputHelper output, string directive) : DirectiveTest(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"); diff --git a/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs b/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs index 0f934946..18da1ee1 100644 --- a/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/YamlFrontMatterTests.cs @@ -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 diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs index b8973606..cb160c69 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/IncludeTests.cs @@ -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( +public class IncludeTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{include} snippets/test.md ``` @@ -35,7 +36,7 @@ public void IncludesInclusionHtml() => } -public class IncludeSubstitutionTests() : DirectiveTest( +public class IncludeSubstitutionTests(ITestOutputHelper output) : DirectiveTest(output, """ --- title: My Document diff --git a/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs b/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs index ac44e619..af3f2f98 100644 --- a/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs +++ b/tests/Elastic.Markdown.Tests/FileInclusion/LiteralIncludeTests.cs @@ -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( +public class LiteralIncludeUsingPropertyTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{include} snippets/test.txt :literal: true @@ -35,7 +36,7 @@ public void IncludesInclusionHtml() => } -public class LiteralIncludeTests() : DirectiveTest( +public class LiteralIncludeTests(ITestOutputHelper output) : DirectiveTest(output, """ ```{literalinclude} snippets/test.md ``` diff --git a/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs b/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs index becabf1b..42304b62 100644 --- a/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/CommentTest.cs @@ -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 diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs b/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs index cceec6e3..173d3ee3 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineImageTest.cs @@ -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( +public class InlineImageTest(ITestOutputHelper output) : InlineTest(output, """ ![Elasticsearch](/_static/img/observability.png){w=350px align=center} """ diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index bb8e3f9d..58862df8 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -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([LanguageInjection("markdown")]string content) : InlineTest(content) +public abstract class LeafTest(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([LanguageInjection("markdown")]string content) : InlineTest(content) +public abstract class InlineTest(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 { { "docs/source/index.md", new MockFileData(content) } diff --git a/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs b/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs index ce8156fa..bf4bb56e 100644 --- a/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs +++ b/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs @@ -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( +public class SubstitutionTest(ITestOutputHelper output) : LeafTest(output, """ --- sub: @@ -30,7 +31,7 @@ public void GeneratesAttributesInHtml() => ); } -public class NeedsDoubleBrackets() : InlineTest( +public class NeedsDoubleBrackets(ITestOutputHelper output) : InlineTest(output, """ --- sub: diff --git a/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs b/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs index dec82f18..8b1f3f61 100644 --- a/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs +++ b/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs @@ -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 { { "docs/source/index.md", new MockFileData("test") } diff --git a/tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs b/tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs index 10c23beb..2c158dea 100644 --- a/tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs +++ b/tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs @@ -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 { diff --git a/tests/Elastic.Markdown.Tests/TestLogger.cs b/tests/Elastic.Markdown.Tests/TestLogger.cs new file mode 100644 index 00000000..4c3d11b0 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/TestLogger.cs @@ -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 state) where TState : notnull => new NullScope(); + + public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Trace; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func 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); +}