diff --git a/docs-builder.sln.DotSettings b/docs-builder.sln.DotSettings
index bf2f2c73d..aca4c0cce 100644
--- a/docs-builder.sln.DotSettings
+++ b/docs-builder.sln.DotSettings
@@ -1,3 +1,4 @@
+ True
True
True
\ No newline at end of file
diff --git a/docs/source/development/index.md b/docs/source/development/index.md
new file mode 100644
index 000000000..9ee2868d5
--- /dev/null
+++ b/docs/source/development/index.md
@@ -0,0 +1,6 @@
+---
+title: Development Guide
+navigation_title: Development
+---
+
+TODO write development documentation here
\ No newline at end of file
diff --git a/docs/source/development/toc.yml b/docs/source/development/toc.yml
new file mode 100644
index 000000000..dec679b99
--- /dev/null
+++ b/docs/source/development/toc.yml
@@ -0,0 +1,2 @@
+toc:
+ - file: index.md
diff --git a/docs/source/docset.yml b/docs/source/docset.yml
index f4f7e75b0..c5deef422 100644
--- a/docs/source/docset.yml
+++ b/docs/source/docset.yml
@@ -69,6 +69,9 @@ toc:
- file: tabs.md
- file: tagged_regions.md
- file: titles.md
+ # nested TOCs are only allowed from docset.yml
+ # to prevent them from being nested deeply arbitrarily
+ - toc: development
- folder: testing
children:
- file: index.md
diff --git a/src/Elastic.Markdown/IO/ConfigurationFile.cs b/src/Elastic.Markdown/IO/ConfigurationFile.cs
index 5d498413a..1a8bf1591 100644
--- a/src/Elastic.Markdown/IO/ConfigurationFile.cs
+++ b/src/Elastic.Markdown/IO/ConfigurationFile.cs
@@ -15,6 +15,7 @@ public record ConfigurationFile : DocumentationFile
private readonly IFileInfo _sourceFile;
private readonly IDirectoryInfo _rootPath;
private readonly BuildContext _context;
+ private readonly int _depth;
public string? Project { get; }
public Glob[] Exclude { get; } = [];
@@ -28,12 +29,13 @@ public record ConfigurationFile : DocumentationFile
private readonly Dictionary _substitutions = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyDictionary Substitutions => _substitutions;
- public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context)
+ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context, int depth = 0, string parentPath = "")
: base(sourceFile, rootPath)
{
_sourceFile = sourceFile;
_rootPath = rootPath;
_context = context;
+ _depth = depth;
if (!sourceFile.Exists)
{
Project = "unknown";
@@ -81,7 +83,13 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
ExternalLinkHosts.Add(host);
break;
case "toc":
- var entries = ReadChildren(entry, string.Empty);
+ if (depth > 1)
+ {
+ EmitError($"toc.yml files may only be linked from docset.yml", entry.Key);
+ break;
+ }
+
+ var entries = ReadChildren(entry, parentPath);
TableOfContents = entries;
break;
@@ -115,20 +123,19 @@ private List ReadChildren(KeyValuePair entry, stri
return entries;
}
- foreach (var tocEntry in sequence.Children.OfType())
- {
- var tocItem = ReadChild(tocEntry, parentPath);
- if (tocItem is not null)
- entries.Add(tocItem);
- }
+ entries.AddRange(
+ sequence.Children.OfType()
+ .SelectMany(tocEntry => ReadChild(tocEntry, parentPath) ?? [])
+ );
return entries;
}
- private ITocItem? ReadChild(YamlMappingNode tocEntry, string parentPath)
+ private IEnumerable? ReadChild(YamlMappingNode tocEntry, string parentPath)
{
string? file = null;
string? folder = null;
+ ConfigurationFile? toc = null;
var fileFound = false;
var folderFound = false;
IReadOnlyCollection? children = null;
@@ -137,6 +144,9 @@ private List ReadChildren(KeyValuePair entry, stri
var key = ((YamlScalarNode)entry.Key).Value;
switch (key)
{
+ case "toc":
+ toc = ReadNestedToc(entry, parentPath, out fileFound);
+ break;
case "file":
file = ReadFile(entry, parentPath, out fileFound);
break;
@@ -150,15 +160,23 @@ private List ReadChildren(KeyValuePair entry, stri
}
}
+ if (toc is not null)
+ {
+ foreach (var f in toc.Files)
+ Files.Add(f);
+
+ return [new FolderReference($"{parentPath}".TrimStart('/'), folderFound, toc.TableOfContents)];
+ }
+
if (file is not null)
- return new TocFile($"{parentPath}/{file}".TrimStart('/'), fileFound, children ?? []);
+ return [new FileReference($"{parentPath}/{file}".TrimStart('/'), fileFound, children ?? [])];
if (folder is not null)
{
if (children is null)
ImplicitFolders.Add(parentPath.TrimStart('/'));
- return new TocFolder($"{parentPath}".TrimStart('/'), folderFound, children ?? []);
+ return [new FolderReference($"{parentPath}".TrimStart('/'), folderFound, children ?? [])];
}
return null;
@@ -226,6 +244,29 @@ private Dictionary ReadDictionary(KeyValuePair entry, string parentPath, out bool found)
+ {
+ found = false;
+ var tocPath = ReadString(entry);
+ if (tocPath is null)
+ {
+ EmitError($"Empty toc: reference", entry.Key);
+ return null;
+ }
+
+ var rootPath = _context.ReadFileSystem.DirectoryInfo.New(Path.Combine(_rootPath.FullName, tocPath));
+ var path = Path.Combine(rootPath.FullName, "toc.yml");
+ var source = _context.ReadFileSystem.FileInfo.New(path);
+ if (!source.Exists)
+ EmitError($"Nested toc: '{source.FullName}' does not exist", entry.Key);
+ else
+ found = true;
+
+ var nestedConfiguration = new ConfigurationFile(source, _rootPath, _context, _depth + 1, tocPath);
+ return nestedConfiguration;
+ }
+
+
private string? ReadString(KeyValuePair entry)
{
if (entry.Value is YamlScalarNode scalar)
diff --git a/src/Elastic.Markdown/IO/DocumentationFolder.cs b/src/Elastic.Markdown/IO/DocumentationFolder.cs
index 3991d60d8..9207d7b7c 100644
--- a/src/Elastic.Markdown/IO/DocumentationFolder.cs
+++ b/src/Elastic.Markdown/IO/DocumentationFolder.cs
@@ -33,10 +33,7 @@ public DocumentationFolder(
Index = index ?? foundIndex;
if (Index is not null)
- {
FilesInOrder = FilesInOrder.Except([Index]).ToList();
- Index.Parent ??= this;
- }
OwnFiles = [.. FilesInOrder];
}
@@ -55,7 +52,7 @@ out List filesInOrder
MarkdownFile? index = null;
foreach (var tocItem in toc)
{
- if (tocItem is TocFile file)
+ if (tocItem is FileReference file)
{
if (!lookup.TryGetValue(file.Path, out var d) || d is not MarkdownFile md)
continue;
@@ -76,14 +73,14 @@ out List filesInOrder
if (file.Path.EndsWith("index.md") && d is MarkdownFile i)
index ??= i;
}
- else if (tocItem is TocFolder folder)
+ else if (tocItem is FolderReference folder)
{
var children = folder.Children;
if (children.Count == 0
&& folderLookup.TryGetValue(folder.Path, out var documentationFiles))
{
children = documentationFiles
- .Select(d => new TocFile(d.RelativePath, true, []))
+ .Select(d => new FileReference(d.RelativePath, true, []))
.ToArray();
}
diff --git a/src/Elastic.Markdown/IO/ITocItem.cs b/src/Elastic.Markdown/IO/ITocItem.cs
index cbc0aa92a..803ffedfa 100644
--- a/src/Elastic.Markdown/IO/ITocItem.cs
+++ b/src/Elastic.Markdown/IO/ITocItem.cs
@@ -6,6 +6,8 @@ namespace Elastic.Markdown.IO;
public interface ITocItem;
-public record TocFile(string Path, bool Found, IReadOnlyCollection Children) : ITocItem;
+public record FileReference(string Path, bool Found, IReadOnlyCollection Children) : ITocItem;
-public record TocFolder(string Path, bool Found, IReadOnlyCollection Children) : ITocItem;
+public record FolderReference(string Path, bool Found, IReadOnlyCollection Children) : ITocItem;
+
+public record TocReference(string Path, bool Found, IReadOnlyCollection Children) : ITocItem;
diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs
index 0e287bb27..aaff75eff 100644
--- a/src/Elastic.Markdown/IO/MarkdownFile.cs
+++ b/src/Elastic.Markdown/IO/MarkdownFile.cs
@@ -31,7 +31,11 @@ public MarkdownFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParse
private DiagnosticsCollector Collector { get; }
- public DocumentationFolder? Parent { get; set; }
+ public DocumentationFolder? Parent
+ {
+ get => FileName == "index.md" ? _parent?.Parent : _parent;
+ set => _parent = value;
+ }
public string? UrlPathPrefix { get; }
private MarkdownParser MarkdownParser { get; }
@@ -55,6 +59,7 @@ public string? NavigationTitle
public string Url => $"{UrlPathPrefix}/{RelativePath.Replace(".md", ".html")}";
private bool _instructionsParsed;
+ private DocumentationFolder? _parent;
public MarkdownFile[] YieldParents()
{
diff --git a/tests/Elastic.Markdown.Tests/SiteMap/BreadCrumbTests.cs b/tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs
similarity index 94%
rename from tests/Elastic.Markdown.Tests/SiteMap/BreadCrumbTests.cs
rename to tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs
index d5f85bc37..c6df8e3d4 100644
--- a/tests/Elastic.Markdown.Tests/SiteMap/BreadCrumbTests.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs
@@ -6,7 +6,7 @@
using FluentAssertions;
using Xunit.Abstractions;
-namespace Elastic.Markdown.Tests.SiteMap;
+namespace Elastic.Markdown.Tests.DocSet;
public class BreadCrumbTests(ITestOutputHelper output) : NavigationTestsBase(output)
{
diff --git a/tests/Elastic.Markdown.Tests/SiteMap/LinkReferenceTests.cs b/tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs
similarity index 95%
rename from tests/Elastic.Markdown.Tests/SiteMap/LinkReferenceTests.cs
rename to tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs
index 38968cc7f..f48b2bc27 100644
--- a/tests/Elastic.Markdown.Tests/SiteMap/LinkReferenceTests.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs
@@ -6,7 +6,7 @@
using FluentAssertions;
using Xunit.Abstractions;
-namespace Elastic.Markdown.Tests.SiteMap;
+namespace Elastic.Markdown.Tests.DocSet;
public class LinkReferenceTests(ITestOutputHelper output) : NavigationTestsBase(output)
{
diff --git a/tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs b/tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs
similarity index 95%
rename from tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs
rename to tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs
index c93d7e557..1b324aae2 100644
--- a/tests/Elastic.Markdown.Tests/SiteMap/NavigationTests.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs
@@ -5,7 +5,7 @@
using FluentAssertions;
using Xunit.Abstractions;
-namespace Elastic.Markdown.Tests.SiteMap;
+namespace Elastic.Markdown.Tests.DocSet;
public class NavigationTests(ITestOutputHelper output) : NavigationTestsBase(output)
{
diff --git a/tests/Elastic.Markdown.Tests/SiteMap/NavigationTestsBase.cs b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs
similarity index 97%
rename from tests/Elastic.Markdown.Tests/SiteMap/NavigationTestsBase.cs
rename to tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs
index f1cd126e3..e34b41e9f 100644
--- a/tests/Elastic.Markdown.Tests/SiteMap/NavigationTestsBase.cs
+++ b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs
@@ -9,7 +9,7 @@
using FluentAssertions;
using Xunit.Abstractions;
-namespace Elastic.Markdown.Tests.SiteMap;
+namespace Elastic.Markdown.Tests.DocSet;
public class NavigationTestsBase : IAsyncLifetime
{
diff --git a/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs b/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs
new file mode 100644
index 000000000..39210e6bc
--- /dev/null
+++ b/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs
@@ -0,0 +1,31 @@
+// 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.IO;
+using FluentAssertions;
+using Xunit.Abstractions;
+
+namespace Elastic.Markdown.Tests.DocSet;
+
+public class NestedTocTests(ITestOutputHelper output) : NavigationTestsBase(output)
+{
+ [Fact]
+ public void InjectsNestedTocsIntoDocumentationSet()
+ {
+ var doc = Generator.DocumentationSet.Files.FirstOrDefault(f => f.RelativePath == "development/index.md") as MarkdownFile;
+
+ doc.Should().NotBeNull();
+
+ // ensure we link back up to main toc in docset yaml
+ doc!.Parent.Should().NotBeNull();
+
+ // its parent should be null
+ doc.Parent!.Parent.Should().BeNull();
+
+ // its parent should point to an index
+ doc.Parent.Index.Should().NotBeNull();
+ doc.Parent.Index!.RelativePath.Should().Be("index.md");
+
+ }
+}