Skip to content

Commit b355058

Browse files
authored
Assembler: Inject global navigation into the build (#822)
* stage * stage * stage * stage * stage * stage * fix output path bug * use url resolver for markdown urls * render reference * post merge build fixes * toggle js a bit * Add temporary TocReferences in navigation even if toc.yml does not exist * fix duplicate reference output * HTML templates keep track of their own depth * Fix sitenav rendering with help of @reakaleek * Tests require checkout folder, skipping for now * attempt windows test failure
1 parent e6f7944 commit b355058

36 files changed

+1373
-503
lines changed

src/Elastic.Markdown/Assets/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const { getOS } = new UAParser();
1616

1717
// Don't remove style tags because they are used by the elastic global nav.
1818
document.addEventListener('htmx:removingHeadElement', function(event) {
19-
if (event.detail.headElement.tagName === 'STYLE') {
19+
const tagName = event.detail.headElement.tagName;
20+
if (tagName === 'STYLE') {
2021
event.preventDefault();
2122
}
2223
});

src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ public interface ICrossLinkResolver
4040
{
4141
Task<FetchedCrossLinks> FetchLinks();
4242
bool TryResolve(Action<string> errorEmitter, Action<string> warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri);
43+
IUriEnvironmentResolver UriResolver { get; }
4344
}
4445

4546
public class CrossLinkResolver(CrossLinkFetcher fetcher, IUriEnvironmentResolver? uriResolver = null) : ICrossLinkResolver
4647
{
4748
private FetchedCrossLinks _crossLinks = FetchedCrossLinks.Empty;
48-
private readonly IUriEnvironmentResolver _uriResolver = uriResolver ?? new IsolatedBuildEnvironmentUriResolver();
49+
public IUriEnvironmentResolver UriResolver { get; } = uriResolver ?? new IsolatedBuildEnvironmentUriResolver();
4950

5051
public async Task<FetchedCrossLinks> FetchLinks()
5152
{
@@ -54,7 +55,7 @@ public async Task<FetchedCrossLinks> FetchLinks()
5455
}
5556

5657
public bool TryResolve(Action<string> errorEmitter, Action<string> warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
57-
TryResolve(errorEmitter, warningEmitter, _crossLinks, _uriResolver, crossLinkUri, out resolvedUri);
58+
TryResolve(errorEmitter, warningEmitter, _crossLinks, UriResolver, crossLinkUri, out resolvedUri);
5859

5960
public FetchedCrossLinks UpdateLinkReference(string repository, LinkReference linkReference)
6061
{

src/Elastic.Markdown/DocumentationGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class DocumentationGenerator
4141
public DocumentationGenerator(
4242
DocumentationSet docSet,
4343
ILoggerFactory logger,
44+
INavigationHtmlWriter? navigationHtmlWriter = null,
4445
IDocumentationFileOutputProvider? documentationFileOutputProvider = null,
4546
IDocumentationFileExporter? documentationExporter = null,
4647
IConversionCollector? conversionCollector = null
@@ -54,7 +55,7 @@ public DocumentationGenerator(
5455
DocumentationSet = docSet;
5556
Context = docSet.Build;
5657
Resolver = docSet.LinkResolver;
57-
HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator());
58+
HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator(), navigationHtmlWriter);
5859
_documentationFileExporter =
5960
documentationExporter
6061
?? docSet.Build.Configuration.EnabledExtensions.FirstOrDefault(e => e.FileExporter != null)?.FileExporter

src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public record ConfigurationFile : DocumentationFile, ITableOfContentsScope
5252
public bool DevelopmentDocs { get; }
5353

5454
// TODO ensure project key is `docs-content`
55-
private bool IsNarrativeDocs =>
55+
public bool IsNarrativeDocs =>
5656
Project is not null
5757
&& Project.Equals("Elastic documentation", StringComparison.OrdinalIgnoreCase);
5858

@@ -119,23 +119,9 @@ public ConfigurationFile(BuildContext context)
119119
}
120120
}
121121

122-
//we read it twice to ensure we read 'toc' last
123-
reader = new YamlStreamReader(sourceFile, _context.Collector);
124-
foreach (var entry in reader.Read())
125-
{
126-
switch (entry.Key)
127-
{
128-
case "toc":
129-
var toc = new TableOfContentsConfiguration(this, ScopeDirectory, _context, 0, "");
130-
var children = toc.ReadChildren(reader, entry.Entry);
131-
var tocEntries = children.OfType<TocReference>().ToArray();
132-
if (!DevelopmentDocs && !IsNarrativeDocs && tocEntries.Length > 0 && children.Count != tocEntries.Length)
133-
reader.EmitError("toc links to other toc sections it may only contain other toc references", entry.Key);
134-
TableOfContents = children;
135-
Files = toc.Files; //side-effect ripe for refactor
136-
break;
137-
}
138-
}
122+
var toc = new TableOfContentsConfiguration(this, sourceFile, ScopeDirectory, _context, 0, "");
123+
TableOfContents = toc.TableOfContents;
124+
Files = toc.Files;
139125
}
140126
catch (Exception e)
141127
{

src/Elastic.Markdown/IO/Configuration/ITocItem.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@ public record FileReference(ITableOfContentsScope TableOfContentsScope, string P
1515
public record FolderReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, IReadOnlyCollection<ITocItem> Children)
1616
: ITocItem;
1717

18-
public record TocReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, IReadOnlyCollection<ITocItem> Children)
19-
: FolderReference(TableOfContentsScope, Path, Found, Children);
18+
public record TocReference(Uri Source, ITableOfContentsScope TableOfContentsScope, string Path, bool Found, IReadOnlyCollection<ITocItem> Children)
19+
: FolderReference(TableOfContentsScope, Path, Found, Children)
20+
21+
{
22+
public IReadOnlyDictionary<Uri, TocReference> TocReferences { get; } =
23+
Children.OfType<TocReference>().ToDictionary(kv => kv.Source, kv => kv);
24+
};

src/Elastic.Markdown/IO/Configuration/TableOfContentsConfiguration.cs

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.IO.Abstractions;
66
using System.Runtime.InteropServices;
77
using Elastic.Markdown.Extensions.DetectionRules;
8-
using Elastic.Markdown.IO.Navigation;
98
using YamlDotNet.RepresentationModel;
109

1110
namespace Elastic.Markdown.IO.Configuration;
@@ -15,6 +14,18 @@ public interface ITableOfContentsScope
1514
IDirectoryInfo ScopeDirectory { get; }
1615
}
1716

17+
public static class ContentSourceMoniker
18+
{
19+
public static Uri Create(string repo, string? path) => new(CreateString(repo, path));
20+
21+
public static string CreateString(string repo, string? path)
22+
{
23+
if (string.IsNullOrWhiteSpace(path))
24+
return $"{repo}://";
25+
return $"{repo}://{path.Replace("\\", "/").Trim('/')}/";
26+
}
27+
}
28+
1829
public record TableOfContentsConfiguration : ITableOfContentsScope
1930
{
2031
private readonly BuildContext _context;
@@ -24,24 +35,64 @@ public record TableOfContentsConfiguration : ITableOfContentsScope
2435
private readonly IDirectoryInfo _rootPath;
2536
private readonly ConfigurationFile _configuration;
2637

38+
public Uri Source { get; }
39+
2740
public HashSet<string> Files { get; } = new(StringComparer.OrdinalIgnoreCase);
2841

2942
public IReadOnlyCollection<ITocItem> TableOfContents { get; private set; } = [];
3043

44+
public IFileInfo DefinitionFile { get; }
3145
public IDirectoryInfo ScopeDirectory { get; }
3246

33-
public TableOfContentsConfiguration(ConfigurationFile configuration, IDirectoryInfo scope, BuildContext context, int depth, string parentPath)
47+
public TableOfContentsConfiguration(
48+
ConfigurationFile configuration,
49+
IFileInfo definitionFile,
50+
IDirectoryInfo scope,
51+
BuildContext context,
52+
int depth,
53+
string parentPath)
3454
{
3555
_configuration = configuration;
56+
DefinitionFile = definitionFile;
3657
ScopeDirectory = scope;
3758
_maxTocDepth = configuration.MaxTocDepth;
3859
_rootPath = context.DocumentationSourceDirectory;
3960
_context = context;
4061
_depth = depth;
4162
_parentPath = parentPath;
63+
64+
var tocPath = scope.FullName;
65+
var relativePath = Path.GetRelativePath(context.DocumentationSourceDirectory.FullName, tocPath);
66+
var moniker = ContentSourceMoniker.Create(context.Git.RepositoryName, relativePath);
67+
Source = moniker;
68+
69+
TableOfContents = ReadChildren();
70+
71+
}
72+
73+
private IReadOnlyCollection<ITocItem> ReadChildren()
74+
{
75+
if (!DefinitionFile.Exists)
76+
return [];
77+
var reader = new YamlStreamReader(DefinitionFile, _context.Collector);
78+
foreach (var entry in reader.Read())
79+
{
80+
switch (entry.Key)
81+
{
82+
case "toc":
83+
var children = ReadChildren(reader, entry.Entry);
84+
var tocEntries = TableOfContents.OfType<TocReference>().ToArray();
85+
if (!_configuration.DevelopmentDocs && !_configuration.IsNarrativeDocs && tocEntries.Length > 0 && TableOfContents.Count != tocEntries.Length)
86+
reader.EmitError("toc links to other toc sections it may only contain other toc references", entry.Key);
87+
return children;
88+
}
89+
}
90+
91+
92+
return [];
4293
}
4394

44-
public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyValuePair<YamlNode, YamlNode> entry, string? parentPath = null)
95+
private IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyValuePair<YamlNode, YamlNode> entry, string? parentPath = null)
4596
{
4697
parentPath ??= _parentPath;
4798
if (_depth > _maxTocDepth)
@@ -118,7 +169,7 @@ public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyVa
118169
foreach (var f in toc.Files)
119170
_ = Files.Add(f);
120171

121-
return [new TocReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, toc.TableOfContents)];
172+
return [new TocReference(toc.Source, toc, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, toc.TableOfContents)];
122173
}
123174

124175
if (file is not null)
@@ -244,7 +295,7 @@ public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyVa
244295
switch (kv.Key)
245296
{
246297
case "toc":
247-
var nestedConfiguration = new TableOfContentsConfiguration(_configuration, source.Directory!, _context, _depth + 1, fullTocPath);
298+
var nestedConfiguration = new TableOfContentsConfiguration(_configuration, source, source.Directory!, _context, _depth + 1, fullTocPath);
248299
_ = nestedConfiguration.ReadChildren(reader, kv.Entry);
249300
return nestedConfiguration;
250301
}

src/Elastic.Markdown/IO/DocumentationSet.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public record NavigationLookups : INavigationLookups
3030
public required IReadOnlyCollection<ITocItem> TableOfContents { get; init; }
3131
public required IReadOnlyCollection<IDocsBuilderExtension> EnabledExtensions { get; init; }
3232
public required FrozenDictionary<string, DocumentationFile[]> FilesGroupedByFolder { get; init; }
33+
//public required FrozenDictionary<Uri, TableOfContentsReference> IndexedTableOfContents { get; init; }
3334
}
3435

3536
public class DocumentationSet : INavigationLookups
@@ -50,7 +51,9 @@ public class DocumentationSet : INavigationLookups
5051

5152
public ICrossLinkResolver LinkResolver { get; }
5253

53-
public DocumentationGroup Tree { get; }
54+
public TableOfContentsTree Tree { get; }
55+
56+
public Uri Source { get; }
5457

5558
public IReadOnlyCollection<DocumentationFile> Files { get; }
5659

@@ -62,14 +65,22 @@ public class DocumentationSet : INavigationLookups
6265

6366
IReadOnlyCollection<IDocsBuilderExtension> INavigationLookups.EnabledExtensions => Configuration.EnabledExtensions;
6467

65-
public DocumentationSet(BuildContext build, ILoggerFactory logger, ICrossLinkResolver? linkResolver = null)
68+
// FrozenDictionary<Uri, TableOfContentsReference>? indexedTableOfContents = null
69+
public DocumentationSet(
70+
BuildContext build,
71+
ILoggerFactory logger,
72+
ICrossLinkResolver? linkResolver = null,
73+
TableOfContentsTreeCollector? treeCollector = null
74+
)
6675
{
6776
Build = build;
77+
Source = ContentSourceMoniker.Create(build.Git.RepositoryName, null);
6878
SourceDirectory = build.DocumentationSourceDirectory;
6979
OutputDirectory = build.DocumentationOutputDirectory;
7080
LinkResolver =
7181
linkResolver ?? new CrossLinkResolver(new ConfigurationCrossLinkFetcher(build.Configuration, logger));
7282
Configuration = build.Configuration;
83+
treeCollector ??= new TableOfContentsTreeCollector();
7384

7485
var resolver = new ParserResolvers
7586
{
@@ -106,10 +117,11 @@ public DocumentationSet(BuildContext build, ILoggerFactory logger, ICrossLinkRes
106117
FlatMappedFiles = FlatMappedFiles,
107118
TableOfContents = Configuration.TableOfContents,
108119
EnabledExtensions = Configuration.EnabledExtensions,
109-
FilesGroupedByFolder = FilesGroupedByFolder
120+
FilesGroupedByFolder = FilesGroupedByFolder,
121+
//IndexedTableOfContents = indexedTableOfContents ?? new Dictionary<Uri, TableOfContentsReference>().ToFrozenDictionary()
110122
};
111123

112-
Tree = new DocumentationGroup(Build, lookups, ref fileIndex);
124+
Tree = new TableOfContentsTree(this, Source, Build, lookups, treeCollector, ref fileIndex);
113125

114126
var markdownFiles = Files.OfType<MarkdownFile>().ToArray();
115127

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.IO.Abstractions;
66
using System.Runtime.InteropServices;
7+
using Elastic.Markdown.CrossLinks;
78
using Elastic.Markdown.Diagnostics;
89
using Elastic.Markdown.Helpers;
910
using Elastic.Markdown.IO.Configuration;
@@ -49,12 +50,12 @@ DocumentationSet set
4950
//may be updated by DocumentationGroup.ProcessTocItems
5051
//todo refactor mutability of MarkdownFile as a whole
5152
ScopeDirectory = build.Configuration.ScopeDirectory;
52-
RootNavigation = set.Tree;
53+
NavigationRoot = set.Tree;
5354
}
5455

5556
public IDirectoryInfo ScopeDirectory { get; set; }
5657

57-
public INavigation RootNavigation { get; set; }
58+
public INavigation NavigationRoot { get; set; }
5859

5960
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
6061

@@ -100,16 +101,39 @@ public string? NavigationTitle
100101

101102
protected virtual string RelativePathUrl => RelativePath;
102103

103-
public string Url
104+
private string DefaultUrlPathSuffix
104105
{
105106
get
106107
{
107108
var relativePath = RelativePathUrl;
108109
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
109110
relativePath = relativePath.Replace('\\', '/');
110111
return Path.GetFileName(relativePath) == "index.md"
111-
? $"{UrlPathPrefix}/{relativePath.Remove(relativePath.LastIndexOf("index.md", StringComparison.Ordinal), "index.md".Length)}"
112-
: $"{UrlPathPrefix}/{relativePath.Remove(relativePath.LastIndexOf(SourceFile.Extension, StringComparison.Ordinal), SourceFile.Extension.Length)}";
112+
? $"/{relativePath.Remove(relativePath.LastIndexOf("index.md", StringComparison.Ordinal), "index.md".Length)}"
113+
: $"/{relativePath.Remove(relativePath.LastIndexOf(SourceFile.Extension, StringComparison.Ordinal), SourceFile.Extension.Length)}";
114+
}
115+
}
116+
117+
private string DefaultUrlPath => $"{UrlPathPrefix}{DefaultUrlPathSuffix}";
118+
119+
private string? _url;
120+
public string Url
121+
{
122+
get
123+
{
124+
if (_url is not null)
125+
return _url;
126+
if (_set.LinkResolver.UriResolver is IsolatedBuildEnvironmentUriResolver)
127+
{
128+
_url = DefaultUrlPath;
129+
return _url;
130+
}
131+
var path = RelativePath;
132+
var crossLink = new Uri($"{_set.Build.Git.RepositoryName}://{path}");
133+
var uri = _set.LinkResolver.UriResolver.Resolve(crossLink, DefaultUrlPathSuffix);
134+
_url = uri.AbsolutePath;
135+
return _url;
136+
113137
}
114138
}
115139

0 commit comments

Comments
 (0)