Skip to content

Commit 7aea1aa

Browse files
reakaleekMpdreamz
andauthored
Add primary-nav feature (#636)
* Add latest header design and adjust pages navigation * Fix unintended merge changes * fix * Pass markdownparser to directive html renderer * Change feature flag from UpperCamelCase to kebab-case * Update docs/_docset.yml * Refactor feature flags * Fix case where targetUrl is null or empty * Revert Commands.cs * Don't fail-fast on matrix * Handle the case where there is no root index.md file * Fix * test * ok * fix * Fix --------- Co-authored-by: Martijn Laarman <[email protected]>
1 parent 0dbabfc commit 7aea1aa

36 files changed

+673
-129
lines changed

.github/workflows/smoke-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ jobs:
77
build:
88
runs-on: ubuntu-latest
99
strategy:
10+
fail-fast: false
1011
matrix:
1112
include:
1213
- repository: elastic/docs-content

docs/_docset.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ subs:
99
serverless-short: Serverless
1010
ece: "Elastic Cloud Enterprise"
1111
eck: "Elastic Cloud on Kubernetes"
12+
13+
features:
14+
primary-nav: false
15+
1216
toc:
1317
- file: index.md
1418
- hidden: developer-notes.md

src/Elastic.Markdown/Assets/pages-nav.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ function scrollCurrentNaviItemIntoView(nav: HTMLElement, delay: number) {
1515
const currentNavItem = $('.current', nav);
1616
expandAllParents(currentNavItem);
1717
setTimeout(() => {
18-
1918
if (currentNavItem && !isElementInViewport(currentNavItem)) {
2019
currentNavItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
20+
window.scrollTo(0, 0);
2121
}
2222
}, delay);
2323
}
@@ -36,7 +36,7 @@ export function initNav() {
3636
if (!pagesNav) {
3737
return;
3838
}
39-
const navItems = $$('a[href="' + window.location.pathname + '"]', pagesNav);
39+
const navItems = $$('a[href="' + window.location.pathname + '"], a[href="' + window.location.pathname + '/"]', pagesNav);
4040
navItems.forEach(el => {
4141
el.classList.add('current');
4242
});

src/Elastic.Markdown/Assets/styles.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,19 @@
7272

7373
.sidebar {
7474
.sidebar-nav {
75-
@apply sticky top-22 z-30 overflow-y-auto;
75+
@apply sticky top-21 z-30 overflow-y-auto;
7676
max-height: calc(100vh - var(--spacing) * 22);
77+
scrollbar-gutter: stable;
7778
}
7879

7980
.sidebar-link {
8081
@apply
8182
text-ink-light
8283
hover:text-black
8384
text-sm
84-
leading-[1.2em]
85+
text-wrap
86+
inline-block
87+
leading-[1.3em]
8588
tracking-[-0.02em];
8689
}
8790
}
@@ -169,7 +172,7 @@
169172
}
170173

171174
#pages-nav .current {
172-
@apply text-blue-elastic!;
175+
@apply font-semibold text-blue-elastic!;
173176
}
174177

175178
.markdown-content {

src/Elastic.Markdown/Helpers/Htmx.cs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,70 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Text;
6+
using Elastic.Markdown.IO.Configuration;
7+
58
namespace Elastic.Markdown.Helpers;
69

7-
public class Htmx
10+
public static class Htmx
11+
{
12+
public static string GetHxSelectOob(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl)
13+
{
14+
HashSet<string> selectTargets =
15+
[
16+
"#primary-nav", "#secondary-nav", "#markdown-content", "#toc-nav", "#prev-next-nav", "#breadcrumbs"
17+
];
18+
if (!HasSameTopLevelGroup(pathPrefix, currentUrl, targetUrl) && features.IsPrimaryNavEnabled)
19+
_ = selectTargets.Add("#pages-nav");
20+
return string.Join(',', selectTargets);
21+
}
22+
23+
public static bool HasSameTopLevelGroup(string? pathPrefix, string currentUrl, string targetUrl)
24+
{
25+
if (string.IsNullOrEmpty(targetUrl) || string.IsNullOrEmpty(currentUrl))
26+
return false;
27+
var startIndex = pathPrefix?.Length ?? 0;
28+
29+
if (currentUrl.Length < startIndex)
30+
throw new InvalidUrlException("Current URL is not a valid URL", currentUrl, startIndex);
31+
32+
if (targetUrl.Length < startIndex)
33+
throw new InvalidUrlException("Target URL is not a valid URL", targetUrl, startIndex);
34+
35+
var currentSegments = GetSegments(currentUrl[startIndex..].Trim('/'));
36+
var targetSegments = GetSegments(targetUrl[startIndex..].Trim('/'));
37+
return currentSegments.Length >= 1 && targetSegments.Length >= 1 && currentSegments[0] == targetSegments[0];
38+
}
39+
40+
public static string GetPreload() => "true";
41+
42+
public static string GetHxSwap() => "none";
43+
public static string GetHxPushUrl() => "true";
44+
public static string GetHxIndicator() => "#htmx-indicator";
45+
46+
private static string[] GetSegments(string url) => url.Split('/');
47+
48+
public static string GetHxAttributes(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl)
49+
{
50+
51+
var attributes = new StringBuilder();
52+
_ = attributes.Append($" hx-get={targetUrl}");
53+
_ = attributes.Append($" hx-select-oob={GetHxSelectOob(features, pathPrefix, currentUrl, targetUrl)}");
54+
_ = attributes.Append($" hx-swap={GetHxSwap()}");
55+
_ = attributes.Append($" hx-push-url={GetHxPushUrl()}");
56+
_ = attributes.Append($" hx-indicator={GetHxIndicator()}");
57+
_ = attributes.Append($" preload={GetPreload()}");
58+
return attributes.ToString();
59+
}
60+
}
61+
62+
63+
internal sealed class InvalidUrlException : ArgumentException
864
{
9-
public static string GetHxSelectOob() => "#markdown-content,#toc-nav,#prev-next-nav,#breadcrumbs";
65+
public InvalidUrlException(string message, string url, int startIndex)
66+
: base($"{message} (Url: {url}, StartIndex: {startIndex})")
67+
{
68+
Data["Url"] = url;
69+
Data["StartIndex"] = startIndex;
70+
}
1071
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public record ConfigurationFile : DocumentationFile
3131
private readonly Dictionary<string, string> _substitutions = new(StringComparer.OrdinalIgnoreCase);
3232
public IReadOnlyDictionary<string, string> Substitutions => _substitutions;
3333

34+
private readonly Dictionary<string, bool> _features = new(StringComparer.OrdinalIgnoreCase);
35+
private FeatureFlags? _featureFlags;
36+
public FeatureFlags Features => _featureFlags ??= new FeatureFlags(_features);
37+
3438
public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context, int depth = 0, string parentPath = "")
3539
: base(sourceFile, rootPath)
3640
{
@@ -79,6 +83,9 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
7983

8084
TableOfContents = entries;
8185
break;
86+
case "features":
87+
_features = reader.ReadDictionary(entry.Entry).ToDictionary(k => k.Key, v => bool.Parse(v.Value), StringComparer.OrdinalIgnoreCase);
88+
break;
8289
case "external_hosts":
8390
reader.EmitWarning($"{entry.Key} has been deprecated and will be removed", entry.Key);
8491
break;
@@ -97,6 +104,8 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
97104
Globs = [.. ImplicitFolders.Select(f => Glob.Parse($"{f}/*.md"))];
98105
}
99106

107+
public bool IsFeatureEnabled(string feature) => _features.TryGetValue(feature, out var enabled) && enabled;
108+
100109
private List<ITocItem> ReadChildren(YamlStreamReader reader, KeyValuePair<YamlNode, YamlNode> entry, string parentPath)
101110
{
102111
var entries = new List<ITocItem>();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace Elastic.Markdown.IO.Configuration;
6+
7+
public class FeatureFlags(Dictionary<string, bool> featureFlags)
8+
{
9+
public bool IsPrimaryNavEnabled => IsEnabled("primary-nav");
10+
private bool IsEnabled(string key) => featureFlags.TryGetValue(key, out var value) && value;
11+
}

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,21 @@ private YamlFrontMatter ReadYamlFrontMatter(string raw)
316316
}
317317

318318

319-
public static string CreateHtml(MarkdownDocument document)
319+
public string CreateHtml(MarkdownDocument document)
320320
{
321321
//we manually render title and optionally append an applies block embedded in yaml front matter.
322322
var h1 = document.Descendants<HeadingBlock>().FirstOrDefault(h => h.Level == 1);
323323
if (h1 is not null)
324324
_ = document.Remove(h1);
325325
return document.ToHtml(MarkdownParser.Pipeline);
326326
}
327+
328+
public static string CreateHtml(MarkdownDocument document, MarkdownParser parser)
329+
{
330+
//we manually render title and optionally append an applies block embedded in yaml front matter.
331+
var h1 = document.Descendants<HeadingBlock>().FirstOrDefault(h => h.Level == 1);
332+
if (h1 is not null)
333+
_ = document.Remove(h1);
334+
return document.ToHtml(parser.Pipeline);
335+
}
327336
}

src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace Elastic.Markdown.Myst.Directives;
2121
/// An HTML renderer for a <see cref="DirectiveBlock"/>.
2222
/// </summary>
2323
/// <seealso cref="HtmlObjectRenderer{CustomContainer}" />
24-
public class DirectiveHtmlRenderer : HtmlObjectRenderer<DirectiveBlock>
24+
public class DirectiveHtmlRenderer(MarkdownParser markdownParser) : HtmlObjectRenderer<DirectiveBlock>
2525
{
2626
protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlock)
2727
{
@@ -62,10 +62,10 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo
6262
if (includeBlock.Literal)
6363
WriteLiteralIncludeBlock(renderer, includeBlock);
6464
else
65-
WriteIncludeBlock(renderer, includeBlock);
65+
WriteIncludeBlock(renderer, includeBlock, markdownParser);
6666
return;
6767
case SettingsBlock settingsBlock:
68-
WriteSettingsBlock(renderer, settingsBlock);
68+
WriteSettingsBlock(renderer, settingsBlock, markdownParser);
6969
return;
7070
default:
7171
// if (!string.IsNullOrEmpty(directiveBlock.Info) && !directiveBlock.Info.StartsWith('{'))
@@ -219,28 +219,24 @@ private static void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock
219219
}
220220
}
221221

222-
private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block)
222+
private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block, MarkdownParser parser)
223223
{
224224
if (!block.Found || block.IncludePath is null)
225225
return;
226226

227-
var parser = new MarkdownParser(block.Build, block.Context);
228227
var snippet = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath);
229228
var parentPath = block.Context.MarkdownSourcePath;
230229
var document = parser.ParseSnippetAsync(snippet, parentPath, block.Context.YamlFrontMatter, default).GetAwaiter().GetResult();
231-
var html = document.ToHtml(MarkdownParser.Pipeline);
230+
var html = document.ToHtml(parser.Pipeline);
232231
_ = renderer.Write(html);
233232
}
234233

235-
private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block)
234+
private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block, MarkdownParser parser)
236235
{
237236
if (!block.Found || block.IncludePath is null)
238237
return;
239238

240-
var parser = new MarkdownParser(block.Build, block.Context);
241-
242239
var file = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath);
243-
244240
YamlSettings? settings;
245241
try
246242
{
@@ -264,7 +260,7 @@ private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock bloc
264260
RenderMarkdown = s =>
265261
{
266262
var document = parser.ParseEmbeddedMarkdown(s, block.IncludeFrom, block.Context.YamlFrontMatter);
267-
var html = document.ToHtml(MarkdownParser.Pipeline);
263+
var html = document.ToHtml(parser.Pipeline);
268264
return html;
269265
}
270266
});

src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace Elastic.Markdown.Myst.Directives;
1313

1414
public static class DirectiveMarkdownBuilderExtensions
1515
{
16-
public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline)
16+
public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline, MarkdownParser markdownParser)
1717
{
18-
pipeline.Extensions.AddIfNotAlready<DirectiveMarkdownExtension>();
18+
pipeline.Extensions.AddIfNotAlready(new DirectiveMarkdownExtension(markdownParser));
1919
return pipeline;
2020
}
2121
}
@@ -24,7 +24,7 @@ public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder
2424
/// Extension to allow custom containers.
2525
/// </summary>
2626
/// <seealso cref="IMarkdownExtension" />
27-
public class DirectiveMarkdownExtension : IMarkdownExtension
27+
public class DirectiveMarkdownExtension(MarkdownParser markdownParser) : IMarkdownExtension
2828
{
2929
public void Setup(MarkdownPipelineBuilder pipeline)
3030
{
@@ -53,7 +53,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
5353
if (!renderer.ObjectRenderers.Contains<DirectiveHtmlRenderer>())
5454
{
5555
// Must be inserted before CodeBlockRenderer
56-
_ = renderer.ObjectRenderers.InsertBefore<CodeBlockRenderer>(new DirectiveHtmlRenderer());
56+
_ = renderer.ObjectRenderers.InsertBefore<CodeBlockRenderer>(new DirectiveHtmlRenderer(markdownParser));
5757
}
5858

5959
_ = renderer.ObjectRenderers.Replace<HeadingRenderer>(new SectionedHeadingRenderer());

src/Elastic.Markdown/Myst/MarkdownParser.cs

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

55
using System.IO.Abstractions;
66
using Cysharp.IO;
7+
using Elastic.Markdown.IO.Configuration;
78
using Elastic.Markdown.Myst.CodeBlocks;
89
using Elastic.Markdown.Myst.Comments;
910
using Elastic.Markdown.Myst.Directives;
@@ -101,33 +102,33 @@ private static async Task<MarkdownDocument> ParseAsync(
101102
}
102103

103104
// ReSharper disable once InconsistentNaming
104-
private static MarkdownPipeline? MinimalPipelineCached;
105-
private static MarkdownPipeline MinimalPipeline
105+
private MarkdownPipeline? _minimalPipelineCached;
106+
private MarkdownPipeline MinimalPipeline
106107
{
107108
get
108109
{
109-
if (MinimalPipelineCached is not null)
110-
return MinimalPipelineCached;
110+
if (_minimalPipelineCached is not null)
111+
return _minimalPipelineCached;
111112
var builder = new MarkdownPipelineBuilder()
112113
.UseYamlFrontMatter()
113114
.UseInlineAnchors()
114115
.UseHeadingsWithSlugs()
115-
.UseDirectives();
116+
.UseDirectives(this);
116117

117118
_ = builder.BlockParsers.TryRemove<IndentedCodeBlockParser>();
118-
MinimalPipelineCached = builder.Build();
119-
return MinimalPipelineCached;
119+
_minimalPipelineCached = builder.Build();
120+
return _minimalPipelineCached;
120121
}
121122
}
122123

123124
// ReSharper disable once InconsistentNaming
124-
private static MarkdownPipeline? PipelineCached;
125-
public static MarkdownPipeline Pipeline
125+
private MarkdownPipeline? _pipelineCached;
126+
public MarkdownPipeline Pipeline
126127
{
127128
get
128129
{
129-
if (PipelineCached is not null)
130-
return PipelineCached;
130+
if (_pipelineCached is not null)
131+
return _pipelineCached;
131132

132133
var builder = new MarkdownPipelineBuilder()
133134
.UseInlineAnchors()
@@ -141,15 +142,15 @@ public static MarkdownPipeline Pipeline
141142
.UseYamlFrontMatter()
142143
.UseGridTables()
143144
.UsePipeTables()
144-
.UseDirectives()
145+
.UseDirectives(this)
145146
.UseDefinitionLists()
146147
.UseEnhancedCodeBlocks()
147-
.UseHtmxLinkInlineRenderer()
148+
.UseHtmxLinkInlineRenderer(Build)
148149
.DisableHtml()
149150
.UseHardBreaks();
150151
_ = builder.BlockParsers.TryRemove<IndentedCodeBlockParser>();
151-
PipelineCached = builder.Build();
152-
return PipelineCached;
152+
_pipelineCached = builder.Build();
153+
return _pipelineCached;
153154
}
154155
}
155156

0 commit comments

Comments
 (0)