diff --git a/docs/docset.yml b/docs/docset.yml index a7963cd21..28f960baf 100644 --- a/docs/docset.yml +++ b/docs/docset.yml @@ -105,3 +105,11 @@ toc: children: - file: first-page.md - file: second-page.md + - folder: deeply-nested + children: + - file: index.md + - file: foo.md + - file: bar.md + - folder: baz + children: + - file: qux.md diff --git a/docs/testing/deeply-nested/bar.md b/docs/testing/deeply-nested/bar.md new file mode 100644 index 000000000..e1c4eb242 --- /dev/null +++ b/docs/testing/deeply-nested/bar.md @@ -0,0 +1 @@ +# Bar diff --git a/docs/testing/deeply-nested/baz/qux.md b/docs/testing/deeply-nested/baz/qux.md new file mode 100644 index 000000000..662110940 --- /dev/null +++ b/docs/testing/deeply-nested/baz/qux.md @@ -0,0 +1 @@ +# Qux diff --git a/docs/testing/deeply-nested/foo.md b/docs/testing/deeply-nested/foo.md new file mode 100644 index 000000000..7635c78e1 --- /dev/null +++ b/docs/testing/deeply-nested/foo.md @@ -0,0 +1 @@ +# Foo diff --git a/docs/testing/deeply-nested/index.md b/docs/testing/deeply-nested/index.md new file mode 100644 index 000000000..b132a6009 --- /dev/null +++ b/docs/testing/deeply-nested/index.md @@ -0,0 +1 @@ +# Deeply Nested diff --git a/src/Elastic.Markdown/Assets/fonts.css b/src/Elastic.Markdown/Assets/fonts.css index 005726153..625b7ae43 100644 --- a/src/Elastic.Markdown/Assets/fonts.css +++ b/src/Elastic.Markdown/Assets/fonts.css @@ -1,7 +1,26 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'); +@font-face { + font-family: "Inter"; + src: url("./fonts/InterVariable.woff2") format("woff2"); + font-display: swap; +} +@font-face { + font-family: "Mier B"; + src: url("./fonts/MierB-Regular.woff2") format("woff2"); + font-weight: normal; + font-display: swap; +} + +@font-face { + font-family: "Mier B"; + src: url("./fonts/MierB-Bold.woff2") format("woff2"); + font-weight: bold; + font-display: swap; +} @font-face { font-family: "Mier B"; - src: url("./fonts/MierB-Regular.woff2") format("woff2") + src: url("./fonts/MierB-Demi.woff2") format("woff2"); + font-weight: 600; + font-display: swap; } diff --git a/src/Elastic.Markdown/Assets/fonts/InterVariable.woff2 b/src/Elastic.Markdown/Assets/fonts/InterVariable.woff2 new file mode 100644 index 000000000..22a12b04e Binary files /dev/null and b/src/Elastic.Markdown/Assets/fonts/InterVariable.woff2 differ diff --git a/src/Elastic.Markdown/Assets/fonts/MierB-Bold.woff2 b/src/Elastic.Markdown/Assets/fonts/MierB-Bold.woff2 new file mode 100644 index 000000000..8c68caa21 Binary files /dev/null and b/src/Elastic.Markdown/Assets/fonts/MierB-Bold.woff2 differ diff --git a/src/Elastic.Markdown/Assets/fonts/MierB-Demi.woff2 b/src/Elastic.Markdown/Assets/fonts/MierB-Demi.woff2 new file mode 100644 index 000000000..1a566c9b4 Binary files /dev/null and b/src/Elastic.Markdown/Assets/fonts/MierB-Demi.woff2 differ diff --git a/src/Elastic.Markdown/Assets/hljs.ts b/src/Elastic.Markdown/Assets/hljs.ts new file mode 100644 index 000000000..7e4c9c4da --- /dev/null +++ b/src/Elastic.Markdown/Assets/hljs.ts @@ -0,0 +1,21 @@ +import {mergeHTMLPlugin} from "./hljs-merge-html-plugin"; +import hljs from "highlight.js"; + +hljs.registerLanguage('apiheader', function() { + return { + case_insensitive: true, // language is case-insensitive + keywords: 'GET POST PUT DELETE HEAD OPTIONS PATCH', + contains: [ + hljs.HASH_COMMENT_MODE, + { + className: "subst", // (pathname: path1/path2/dothis) color #ab5656 + begin: /(?<=(?:\/|GET |POST |PUT |DELETE |HEAD |OPTIONS |PATH))[^?\n\r\/]+/, + } + ], } +}) + +hljs.addPlugin(mergeHTMLPlugin); + +export function initHighlight() { + hljs.highlightAll(); +} diff --git a/src/Elastic.Markdown/Assets/main.ts b/src/Elastic.Markdown/Assets/main.ts index 2ea9c46b6..97b9d92b8 100644 --- a/src/Elastic.Markdown/Assets/main.ts +++ b/src/Elastic.Markdown/Assets/main.ts @@ -1,18 +1,5 @@ -import hljs from "highlight.js"; -import {mergeHTMLPlugin} from "./hljs-merge-html-plugin"; +import {initNav} from "./pages-nav"; +import {initHighlight} from "./hljs"; -hljs.registerLanguage('apiheader', function() { - return { - case_insensitive: true, // language is case-insensitive - keywords: 'GET POST PUT DELETE HEAD OPTIONS PATCH', - contains: [ - hljs.HASH_COMMENT_MODE, - { - className: "subst", // (pathname: path1/path2/dothis) color #ab5656 - begin: /(?<=(?:\/|GET |POST |PUT |DELETE |HEAD |OPTIONS |PATH))[^?\n\r\/]+/, - } - ], } -}) - -hljs.addPlugin(mergeHTMLPlugin); -hljs.highlightAll(); +initNav(); +initHighlight(); diff --git a/src/Elastic.Markdown/Assets/pages-nav.ts b/src/Elastic.Markdown/Assets/pages-nav.ts new file mode 100644 index 000000000..96f763bbc --- /dev/null +++ b/src/Elastic.Markdown/Assets/pages-nav.ts @@ -0,0 +1,78 @@ +import {$, $$} from "select-dom/strict"; + +type NavExpandState = { [key: string]: boolean }; +const PAGE_NAV_EXPAND_STATE_KEY = 'pagesNavState'; +const navState = JSON.parse(sessionStorage.getItem(PAGE_NAV_EXPAND_STATE_KEY)) as NavExpandState + +// Initialize the nav state from the session storage +// Return a function to keep the nav state in the session storage that should be called before the page is unloaded +function keepNavState(nav: HTMLElement): () => void { + const inputs = $$('input[type="checkbox"]', nav); + if (navState) { + inputs.forEach(input => { + const key = input.id; + if ('shouldExpand' in input.dataset && input.dataset['shouldExpand'] === 'true') { + input.checked = true; + } else { + input.checked = navState[key]; + } + }); + } + + return () => { + const inputs = $$('input[type="checkbox"]', nav); + const state: NavExpandState = inputs.reduce((state: NavExpandState, input) => { + const key = input.id; + const value = input.checked; + return { ...state, [key]: value}; + }, {}); + sessionStorage.setItem(PAGE_NAV_EXPAND_STATE_KEY, JSON.stringify(state)); + } +} + +type NavScrollPosition = number; +const PAGE_NAV_SCROLL_POSITION_KEY = 'pagesNavScrollPosition'; +const pagesNavScrollPosition: NavScrollPosition = parseInt( + sessionStorage.getItem(PAGE_NAV_SCROLL_POSITION_KEY) ?? '0' +); + + +// Initialize the nav scroll position from the session storage +// Return a function to keep the nav scroll position in the session storage that should be called before the page is unloaded +function keepNavPosition(nav: HTMLElement): () => void { + if (pagesNavScrollPosition) { + nav.scrollTop = pagesNavScrollPosition; + } + return () => { + sessionStorage.setItem(PAGE_NAV_SCROLL_POSITION_KEY, nav.scrollTop.toString()); + } +} + +function scrollCurrentNaviItemIntoView(nav: HTMLElement, delay: number) { + setTimeout(() => { + const currentNavItem = $('.current', nav); + if (currentNavItem && !isElementInViewport(currentNavItem)) { + currentNavItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, delay); +} +function isElementInViewport(el: HTMLElement): boolean { + const rect = el.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} + +export function initNav() { + const pagesNav = $('#pages-nav'); + const keepNavStateCallback = keepNavState(pagesNav); + const keepNavPositionCallback = keepNavPosition(pagesNav); + scrollCurrentNaviItemIntoView(pagesNav, 100); + window.addEventListener('beforeunload', () => { + keepNavStateCallback(); + keepNavPositionCallback(); + }, true); +} diff --git a/src/Elastic.Markdown/Assets/plugins.css b/src/Elastic.Markdown/Assets/plugins.css deleted file mode 100644 index 3df16aafb..000000000 --- a/src/Elastic.Markdown/Assets/plugins.css +++ /dev/null @@ -1 +0,0 @@ -@plugin "tailwind-scrollbar-hide" diff --git a/src/Elastic.Markdown/Assets/styles.css b/src/Elastic.Markdown/Assets/styles.css index 5039e5af7..a663b8466 100644 --- a/src/Elastic.Markdown/Assets/styles.css +++ b/src/Elastic.Markdown/Assets/styles.css @@ -1,12 +1,11 @@ @import "tailwindcss"; @import "./fonts.css"; -@import "./plugins.css"; @import "./theme.css"; @import "highlight.js/styles/atom-one-dark.css"; @import "./markdown/typography.css"; #default-search::-webkit-search-cancel-button { - @apply pr-2; + padding-right: calc(var(--spacing) * 2); -webkit-appearance: none; height: 16px; width: 16px; @@ -15,3 +14,35 @@ cursor: pointer; background-repeat: no-repeat; } + +#pages-nav { + &::-webkit-scrollbar-track { + background-color: transparent; + } + &:hover::-webkit-scrollbar-thumb { + background-color: var(--color-gray-light); + } + &::-webkit-scrollbar { + width: calc(var(--spacing) * 2); + height: calc(var(--spacing) * 2); + } + &::-webkit-scrollbar-thumb { + border-radius: var(--spacing); + } + + scrollbar-gutter: stable; +} + + +#pages-nav li.current { + position: relative; + &::before { + content: ""; + position: absolute; + top: 50%; + left: -1px; + width: calc(var(--spacing) * 6); + height: 1px; + background-color: var(--color-gray-200); + } +} diff --git a/src/Elastic.Markdown/Helpers/BoolExtensions.cs b/src/Elastic.Markdown/Helpers/BoolExtensions.cs new file mode 100644 index 000000000..6982300f4 --- /dev/null +++ b/src/Elastic.Markdown/Helpers/BoolExtensions.cs @@ -0,0 +1,10 @@ +// 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.Helpers; + +public static class BoolExtensions +{ + public static string ToLowerString(this bool @bool) => @bool.ToString().ToLowerInvariant(); +} diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index 3785c2b66..2a2c07d65 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -33,6 +33,13 @@ public class DocumentationGroup public int Depth { get; } + public bool ContainsCurrentPage(MarkdownFile current) => NavigationItems.Any(n => n switch + { + FileNavigation f => f.File == current, + GroupNavigation g => g.Group.ContainsCurrentPage(current), + _ => false + }); + public DocumentationGroup( BuildContext context, IReadOnlyCollection toc, diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml new file mode 100644 index 000000000..1fd63a0d9 --- /dev/null +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -0,0 +1,19 @@ +@inherits RazorSlice +
    +
  1. + + Elastic + + +
  2. + @foreach (var item in Model.Parents.Reverse().Skip(1)) + { +
  3. + / + + @item.NavigationTitle + + +
  4. + } +
diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml new file mode 100644 index 000000000..739db0d21 --- /dev/null +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -0,0 +1,27 @@ +@inherits RazorSlice +
+
+
+ + Elastic + +
+ + +
+
diff --git a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml new file mode 100644 index 000000000..5edb1853b --- /dev/null +++ b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml @@ -0,0 +1,6 @@ +@inherits RazorSlice + diff --git a/src/Elastic.Markdown/Slices/Layout/_TocTree.cshtml b/src/Elastic.Markdown/Slices/Layout/_TocTree.cshtml index 0e2b41843..6751687bd 100644 --- a/src/Elastic.Markdown/Slices/Layout/_TocTree.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_TocTree.cshtml @@ -1,20 +1,37 @@ @inherits RazorSlice - - + + +} diff --git a/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml b/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml index a77fdd9f7..33c3e006a 100644 --- a/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml @@ -1,27 +1,101 @@ +@using Elastic.Markdown.Helpers @using Elastic.Markdown.IO.Navigation @inherits RazorSlice -@foreach (var item in Model.SubTree.NavigationItems) +@if (Model.IsRedesign) { - if (item is FileNavigation file) + @foreach (var item in Model.SubTree.NavigationItems) { - var f = file.File; - var current = f == Model.CurrentDocument ? " current" : string.Empty; -
  • @f.NavigationTitle
  • + if (item is FileNavigation file) + { + var f = file.File; + var isCurrent = f == Model.CurrentDocument; +
  • + +
  • + } + else if (item is GroupNavigation folder) + { + var g = folder.Group; + var isCurrent = g.Index == Model.CurrentDocument; + var slug = g.Index?.Title.Slugify(); + const int initialExpandLevel = 1; + var containsCurrent = g.HoldsCurrent(Model.CurrentDocument) || g.ContainsCurrentPage(Model.CurrentDocument); + var shouldInitiallyExpand = containsCurrent || g.Depth <= initialExpandLevel; +
  • + + @if (g.NavigationItems.Count > 0) + { + + } +
  • + } } - else if (item is GroupNavigation folder) +} +else +{ + @foreach (var item in Model.SubTree.NavigationItems) { - var g = folder.Group; - var current = g.HoldsCurrent(Model.CurrentDocument) ? " current" : string.Empty; - var currentFile = g.Index == Model.CurrentDocument ? " current" : string.Empty; -
  • @g.Index?.NavigationTitle@if (@g.NavigationItems.Count > 0) {
      - @await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem - { - Level = g.Depth, - CurrentDocument = Model.CurrentDocument, - SubTree = g - })) -
    - } -
  • + if (item is FileNavigation file) + { + var f = file.File; + var current = f == Model.CurrentDocument ? " current" : string.Empty; +
  • @f.NavigationTitle
  • + } + else if (item is GroupNavigation folder) + { + var g = folder.Group; + var current = g.HoldsCurrent(Model.CurrentDocument) ? " current" : string.Empty; + var currentFile = g.Index == Model.CurrentDocument ? " current" : string.Empty; +
  • @g.Index?.NavigationTitle@if (@g.NavigationItems.Count > 0) {
      + @await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem + { + Level = g.Depth, + CurrentDocument = Model.CurrentDocument, + SubTree = g + })) +
    + } +
  • + } } } diff --git a/src/Elastic.Markdown/Slices/_Layout.cshtml b/src/Elastic.Markdown/Slices/_Layout.cshtml index 3c0da9a7b..17c0b9e91 100644 --- a/src/Elastic.Markdown/Slices/_Layout.cshtml +++ b/src/Elastic.Markdown/Slices/_Layout.cshtml @@ -10,58 +10,20 @@ - -
    -
    -
    -
    - Elastic -
    - - -
    -
    -
    - -
    -
      -
    1. - - Elastic - - / - -
    2. - @foreach (var item in Model.Parents.Reverse()) - { -
    3. - - @item.NavigationTitle - - / - -
    4. - } -
    + @(await RenderPartialAsync(_Header.Create(Model))) +
    + @await RenderPartialAsync(_PagesNav.Create(Model)) +
    + @await RenderPartialAsync(_Breadcrumbs.Create(Model)) @await RenderBodyAsync() -
    -
    + + + diff --git a/src/Elastic.Markdown/Slices/_ViewModels.cs b/src/Elastic.Markdown/Slices/_ViewModels.cs index d2a3514d3..65d74724a 100644 --- a/src/Elastic.Markdown/Slices/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/_ViewModels.cs @@ -24,7 +24,12 @@ public class IndexViewModel public required bool AllowIndexing { get; init; } } -public class LayoutViewModel +public abstract class RedsignViewModel +{ + public bool IsRedesign => Environment.GetEnvironmentVariable("REDESIGN") == "true"; +} + +public class LayoutViewModel : RedsignViewModel { public string Title { get; set; } = "Elastic Documentation"; public string RawTitle { get; set; } = "Elastic Documentation"; @@ -38,8 +43,6 @@ public class LayoutViewModel public required string? GithubEditUrl { get; set; } public required bool AllowIndexing { get; init; } - public bool IsRedesign => Environment.GetEnvironmentVariable("REDESIGN") == "true"; - private MarkdownFile[]? _parents; public MarkdownFile[] Parents { @@ -73,13 +76,13 @@ public class PageTocItem } -public class NavigationViewModel +public class NavigationViewModel : RedsignViewModel { public required DocumentationGroup Tree { get; init; } public required MarkdownFile CurrentDocument { get; init; } } -public class NavigationTreeItem +public class NavigationTreeItem : RedsignViewModel { public required int Level { get; init; } public required MarkdownFile CurrentDocument { get; init; } diff --git a/src/Elastic.Markdown/package-lock.json b/src/Elastic.Markdown/package-lock.json index f0df56fa9..ba2c40113 100644 --- a/src/Elastic.Markdown/package-lock.json +++ b/src/Elastic.Markdown/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "highlight.js": "^11.11.1", - "tailwind-scrollbar-hide": "^2.0.0", + "select-dom": "^9.3.0", "tailwindcss": "^4.0.3" }, "devDependencies": { @@ -4291,6 +4291,20 @@ } ] }, + "node_modules/select-dom": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/select-dom/-/select-dom-9.3.0.tgz", + "integrity": "sha512-oZkhmrKSdj4pEEpK55P2SK4oVr2ozXhr692kXteqf2d+chwjgjgS57AlUoZaFiwnfZR15aW4+GtGXNYBQgo6yw==", + "dependencies": { + "typed-query-selector": "^2.11.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/fregante" + } + }, "node_modules/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", @@ -4421,14 +4435,6 @@ "node": ">= 10" } }, - "node_modules/tailwind-scrollbar-hide": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-2.0.0.tgz", - "integrity": "sha512-lqiIutHliEiODwBRHy4G2+Tcayo2U7+3+4frBmoMETD72qtah+XhOk5XcPzC1nJvXhXUdfl2ajlMhUc2qC6CIg==", - "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" - } - }, "node_modules/tailwindcss": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.3.tgz", @@ -4491,6 +4497,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", diff --git a/src/Elastic.Markdown/package.json b/src/Elastic.Markdown/package.json index c28e635ed..4ac66c0d5 100644 --- a/src/Elastic.Markdown/package.json +++ b/src/Elastic.Markdown/package.json @@ -34,7 +34,7 @@ ], "dependencies": { "highlight.js": "^11.11.1", - "tailwind-scrollbar-hide": "^2.0.0", + "select-dom": "^9.3.0", "tailwindcss": "^4.0.3" } }