diff --git a/docs-builder.sln b/docs-builder.sln index 51cc9b408..4a08a8478 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -51,6 +51,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs-assembler", "src\docs-assembler\docs-assembler.csproj", "{28350800-B44B-479B-86E2-1D39E321C0B4}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "authoring", "tests\authoring\authoring.fsproj", "{018F959E-824B-4664-B345-066784478D24}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -89,6 +91,10 @@ Global {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.Build.0 = Release|Any CPU + {018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {018F959E-824B-4664-B345-066784478D24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {018F959E-824B-4664-B345-066784478D24}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} @@ -99,5 +105,6 @@ Global {CD2887E3-BDA9-434B-A5BF-9ED38DE20332} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} {A2A34BBC-CB5E-4100-9529-A12B6ECB769C} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} {28350800-B44B-479B-86E2-1D39E321C0B4} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} + {018F959E-824B-4664-B345-066784478D24} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5} EndGlobalSection EndGlobal diff --git a/docs-builder.sln.DotSettings b/docs-builder.sln.DotSettings index aca4c0cce..2cc897060 100644 --- a/docs-builder.sln.DotSettings +++ b/docs-builder.sln.DotSettings @@ -1,4 +1,6 @@  True + True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs index 411229319..500e8bc5e 100644 --- a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs @@ -31,7 +31,7 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) :::{important} :name: hint_ref -This is a 'important' admonition +This is an 'important' admonition ::: """; fileSystem.AddFile(@"docs/testing/req.md", inclusion); diff --git a/tests/authoring/Framework/ErrorCollectorAssertions.fs b/tests/authoring/Framework/ErrorCollectorAssertions.fs new file mode 100644 index 000000000..3c530fa6c --- /dev/null +++ b/tests/authoring/Framework/ErrorCollectorAssertions.fs @@ -0,0 +1,28 @@ +// 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 authoring + +open System.Diagnostics +open System.Linq +open Elastic.Markdown.Diagnostics +open FsUnitTyped +open Swensen.Unquote + +[] +module DiagnosticsCollectorAssertions = + + [] + let hasNoErrors (actual: GenerateResult) = + test <@ actual.Context.Collector.Errors = 0 @> + + [] + let hasError (expected: string) (actual: GenerateResult) = + actual.Context.Collector.Errors |> shouldBeGreaterThan 0 + let errorDiagnostics = actual.Context.Collector.Diagnostics + .Where(fun d -> d.Severity = Severity.Error) + .ToArray() + |> List.ofArray + let message = errorDiagnostics.FirstOrDefault().Message + test <@ message.Contains(expected) @> diff --git a/tests/authoring/Framework/HtmlAssertions.fs b/tests/authoring/Framework/HtmlAssertions.fs new file mode 100644 index 000000000..341bea2dc --- /dev/null +++ b/tests/authoring/Framework/HtmlAssertions.fs @@ -0,0 +1,123 @@ +// 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 authoring + +open System +open System.Diagnostics +open System.IO +open AngleSharp.Diffing +open AngleSharp.Diffing.Core +open AngleSharp.Html +open AngleSharp.Html.Parser +open DiffPlex.DiffBuilder +open DiffPlex.DiffBuilder.Model +open JetBrains.Annotations +open Xunit.Sdk + +[] +module HtmlAssertions = + + let htmlDiffString (diffs: seq) = + let NodeName (source:ComparisonSource) = source.Node.NodeType.ToString().ToLowerInvariant(); + let htmlText (source:IDiff) = + let formatter = PrettyMarkupFormatter(); + let nodeText (control: ComparisonSource) = + use sw = new StringWriter() + control.Node.ToHtml(sw, formatter) + sw.ToString() + let attrText (control: AttributeComparisonSource) = + use sw = new StringWriter() + control.Attribute.ToHtml(sw, formatter) + sw.ToString() + let nodeDiffText (control: ComparisonSource option) (test: ComparisonSource option) = + let actual = match test with Some t -> nodeText t | None -> "missing" + let expected = match control with Some t -> nodeText t | None -> "missing" + $""" + +expected: {expected} +actual: {actual} +""" + let attrDiffText (control: AttributeComparisonSource option) (test: AttributeComparisonSource option) = + let actual = match test with Some t -> attrText t | None -> "missing" + let expected = match control with Some t -> attrText t | None -> "missing" + $""" + +expected: {expected} +actual: {actual} +""" + + match source with + | :? NodeDiff as diff -> nodeDiffText <| Some diff.Control <| Some diff.Test + | :? AttrDiff as diff -> attrDiffText <| Some diff.Control <| Some diff.Test + | :? MissingNodeDiff as diff -> nodeDiffText <| Some diff.Control <| None + | :? MissingAttrDiff as diff -> attrDiffText <| Some diff.Control <| None + | :? UnexpectedNodeDiff as diff -> nodeDiffText None <| Some diff.Test + | :? UnexpectedAttrDiff as diff -> attrDiffText None <| Some diff.Test + | _ -> failwith $"Unknown diff type detected: {source.GetType()}" + + diffs + |> Seq.map (fun diff -> + + match diff with + | :? NodeDiff as diff when diff.Target = DiffTarget.Text && diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal) + -> $"The text in {diff.Control.Path} is different." + | :? NodeDiff as diff when diff.Target = DiffTarget.Text + -> $"The expected {NodeName(diff.Control)} at {diff.Control.Path} and the actual {NodeName(diff.Test)} at {diff.Test.Path} is different." + | :? NodeDiff as diff when diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal) + -> $"The {NodeName(diff.Control)}s at {diff.Control.Path} are different." + | :? NodeDiff as diff -> $"The expected {NodeName(diff.Control)} at {diff.Control.Path} and the actual {NodeName(diff.Test)} at {diff.Test.Path} are different." + | :? AttrDiff as diff when diff.Control.Path.Equals(diff.Test.Path, StringComparison.Ordinal) + -> $"The values of the attributes at {diff.Control.Path} are different." + | :? AttrDiff as diff -> $"The value of the attribute {diff.Control.Path} and actual attribute {diff.Test.Path} are different." + | :? MissingNodeDiff as diff -> $"The {NodeName(diff.Control)} at {diff.Control.Path} is missing." + | :? MissingAttrDiff as diff -> $"The attribute at {diff.Control.Path} is missing." + | :? UnexpectedNodeDiff as diff -> $"The {NodeName(diff.Test)} at {diff.Test.Path} was not expected." + | :? UnexpectedAttrDiff as diff -> $"The attribute at {diff.Test.Path} was not expected." + | _ -> failwith $"Unknown diff type detected: {diff.GetType()}" + + + htmlText diff + ) + |> String.concat "\n" + + let private prettyHtml (html:string) = + let parser = HtmlParser() + let document = parser.ParseDocument(html) + use sw = new StringWriter() + document.Body.Children + |> Seq.iter _.ToHtml(sw, PrettyMarkupFormatter()) + sw.ToString() + + [] + let convertsToHtml ([]expected: string) (actual: GenerateResult) = + let diffs = + DiffBuilder + .Compare(actual.Html) + .WithTest(expected) + .Build() + + let diff = htmlDiffString diffs + match diff with + | s when String.IsNullOrEmpty s -> () + | s -> + let expectedHtml = prettyHtml expected + let actualHtml = prettyHtml actual.Html + let textDiff = + InlineDiffBuilder.Diff(expectedHtml, actualHtml).Lines + |> Seq.map(fun l -> + match l.Type with + | ChangeType.Deleted -> "- " + l.Text + | ChangeType.Modified -> "+ " + l.Text + | ChangeType.Inserted -> "+ " + l.Text + | _ -> " " + l.Text + ) + |> String.concat "\n" + let msg = $"""Html was not equal +{textDiff} + +{diff} +""" + raise (XunitException(msg)) + + diff --git a/tests/authoring/Framework/Setup.fs b/tests/authoring/Framework/Setup.fs new file mode 100644 index 000000000..0debb0b24 --- /dev/null +++ b/tests/authoring/Framework/Setup.fs @@ -0,0 +1,81 @@ +// 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 authoring + +open System.Collections.Generic +open System.IO +open System.IO.Abstractions.TestingHelpers +open System.Threading.Tasks +open Elastic.Markdown +open Elastic.Markdown.IO +open JetBrains.Annotations + +type Setup = + + static let GenerateDocSetYaml( + fileSystem: MockFileSystem, + globalVariables: Dictionary option + ) = + let root = fileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/")); + let yaml = new StringWriter(); + yaml.WriteLine("toc:"); + let markdownFiles = fileSystem.Directory.EnumerateFiles(root.FullName, "*.md", SearchOption.AllDirectories) + markdownFiles + |> Seq.iter(fun markdownFile -> + let relative = fileSystem.Path.GetRelativePath(root.FullName, markdownFile); + yaml.WriteLine($" - file: {relative}"); + ) + match globalVariables with + | Some vars -> + yaml.WriteLine($"subs:") + vars |> Seq.iter(fun kv -> + yaml.WriteLine($" {kv.Key}: {kv.Value}"); + ) + | _ -> () + + fileSystem.AddFile(Path.Combine(root.FullName, "docset.yml"), MockFileData(yaml.ToString())); + + static let Generate ([]m: string) : Task = + + let d = dict [ ("docs/index.md", MockFileData(m)) ] + let opts = MockFileSystemOptions(CurrentDirectory=Paths.Root.FullName) + let fileSystem = MockFileSystem(d, opts) + + GenerateDocSetYaml (fileSystem, None) + + let collector = TestDiagnosticsCollector(); + let context = BuildContext(fileSystem, Collector=collector) + let set = DocumentationSet(context); + let file = + match set.GetMarkdownFile(fileSystem.FileInfo.New("docs/index.md")) with + | NonNull f -> f + | _ -> failwithf "docs/index.md could not be located" + + let context = { + File = file + Collector = collector + Set = set + ReadFileSystem = fileSystem + WriteFileSystem = fileSystem + } + context.Bootstrap() + + /// Pass a full documentation page to the test setup + static member Document ([]m: string) = + let g = task { return! Generate m } + g |> Async.AwaitTask |> Async.RunSynchronously + + /// Pass a markdown fragment to the test setup + static member Markdown ([]m: string) = + // language=markdown + let m = $""" +# Test Document +{m} +""" + let g = task { + return! Generate m + } + g |> Async.AwaitTask |> Async.RunSynchronously + diff --git a/tests/authoring/Framework/TestValues.fs b/tests/authoring/Framework/TestValues.fs new file mode 100644 index 000000000..3c77f2c6d --- /dev/null +++ b/tests/authoring/Framework/TestValues.fs @@ -0,0 +1,70 @@ +// 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 authoring + +open System +open System.IO.Abstractions +open Elastic.Markdown.Diagnostics +open Elastic.Markdown.IO +open Markdig.Syntax +open Xunit + + +type TestDiagnosticsOutput() = + + interface IDiagnosticsOutput with + member this.Write diagnostic = + let line = match diagnostic.Line with | NonNullV l -> l | _ -> 0 + match TestContext.Current.TestOutputHelper with + | NonNull output -> + match diagnostic.Severity with + | Severity.Error -> + output.WriteLine($"Error: {diagnostic.Message} ({diagnostic.File}:{line})") + | _ -> + output.WriteLine($"Warn : {diagnostic.Message} ({diagnostic.File}:{line})") + | _ -> () + + +type TestDiagnosticsCollector() = + inherit DiagnosticsCollector([TestDiagnosticsOutput()]) + + let diagnostics = System.Collections.Generic.List() + + member _.Diagnostics = diagnostics.AsReadOnly() + + override this.HandleItem diagnostic = diagnostics.Add(diagnostic); + +type GenerateResult = { + Document: MarkdownDocument + Html: string + Context: MarkdownTestContext +} + +and MarkdownTestContext = + { + File: MarkdownFile + Collector: TestDiagnosticsCollector + Set: DocumentationSet + ReadFileSystem: IFileSystem + WriteFileSystem: IFileSystem + } + + member this.Bootstrap () = backgroundTask { + let! ctx = Async.CancellationToken + let _ = this.Collector.StartAsync(ctx) + do! this.Set.ResolveDirectoryTree(ctx) + + let! document = this.File.ParseFullAsync(ctx) + + let html = this.File.CreateHtml(document); + this.Collector.Channel.TryComplete() + do! this.Collector.StopAsync(ctx) + return { Context = this; Document = document; Html = html } + } + + interface IDisposable with + member this.Dispose() = () + + diff --git a/tests/authoring/Inline/Comments.fs b/tests/authoring/Inline/Comments.fs new file mode 100644 index 000000000..a05bb87a6 --- /dev/null +++ b/tests/authoring/Inline/Comments.fs @@ -0,0 +1,19 @@ +// 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 + +module ``inline elements``.``comment block`` + +open Xunit +open authoring + +type ``commented line`` () = + + static let markdown = Setup.Markdown """ +% comment +not a comment +""" + + [] + let ``validate HTML: commented line should not be emitted`` () = + markdown |> convertsToHtml """

not a comment

""" diff --git a/tests/authoring/Inline/InlineAnchors.fs b/tests/authoring/Inline/InlineAnchors.fs new file mode 100644 index 000000000..73f9552b5 --- /dev/null +++ b/tests/authoring/Inline/InlineAnchors.fs @@ -0,0 +1,24 @@ +// 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 + +module ``inline elements``.``anchors DEPRECATED`` + +open Xunit +open authoring + +type ``inline anchor in the middle`` () = + + static let markdown = Setup.Markdown """ +this is *regular* text and this $$$is-an-inline-anchor$$$ and this continues to be regular text +""" + + [] + let ``validate HTML`` () = + markdown |> convertsToHtml """ +

this is regular text and this + and this continues to be regular text +

+ """ + [] + let ``has no errors`` () = markdown |> hasNoErrors diff --git a/tests/authoring/Inline/InlineImages.fs b/tests/authoring/Inline/InlineImages.fs new file mode 100644 index 000000000..0680a6841 --- /dev/null +++ b/tests/authoring/Inline/InlineImages.fs @@ -0,0 +1,30 @@ +// 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 + +module ``inline elements``.``image`` + +open Xunit +open authoring + +type ``static path to image`` () = + static let markdown = Setup.Markdown """ +![Elasticsearch](/_static/img/observability.png) +""" + + [] + let ``validate HTML: generates link and alt attr`` () = + markdown |> convertsToHtml """ +

Elasticsearch

+ """ + +type ``relative path to image`` () = + static let markdown = Setup.Markdown """ +![Elasticsearch](_static/img/observability.png) +""" + + [] + let ``validate HTML: preserves relative path`` () = + markdown |> convertsToHtml """ +

Elasticsearch

+ """ diff --git a/tests/authoring/Inline/Substitutions.fs b/tests/authoring/Inline/Substitutions.fs new file mode 100644 index 000000000..ef128c249 --- /dev/null +++ b/tests/authoring/Inline/Substitutions.fs @@ -0,0 +1,51 @@ +// 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 + +module ``inline elements``.``substitutions`` + +open Xunit +open authoring + +type ``read sub from yaml frontmatter`` () = + static let markdown = Setup.Document """--- +sub: + hello-world: "Hello World!" +--- +The following should be subbed: {{hello-world}} +not a comment +""" + + [] + let ``validate HTML: replace substitution`` () = + markdown |> convertsToHtml """ +

The following should be subbed: Hello World!
+not a comment

+ """ + + +type ``requires valid syntax and key to be found`` () = + static let markdown = Setup.Document """--- +sub: + hello-world: "Hello World!" +--- +# Testing substitutions + +The following should be subbed: {{hello-world}} +not a comment +not a {{valid-key}} +not a {substitution} +""" + + [] + let ``emits an error when sub key is not found`` () = + markdown |> hasError "key {valid-key} is undefined" + + [] + let ``validate HTML: leaves non subs alone`` () = + markdown |> convertsToHtml """ +

The following should be subbed: Hello World!
+ not a comment
+ not a {{valid-key}}
+ not a {substitution}

+""" diff --git a/tests/authoring/authoring.fsproj b/tests/authoring/authoring.fsproj new file mode 100644 index 000000000..29d89d23e --- /dev/null +++ b/tests/authoring/authoring.fsproj @@ -0,0 +1,40 @@ + + + + net9.0 + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +