Skip to content

Commit 08bce21

Browse files
committed
Api ref doc sidebar optimization
1 parent e0d58b9 commit 08bce21

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

build.gradle.kts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import aws.sdk.kotlin.gradle.dsl.configureLinting
66
import aws.sdk.kotlin.gradle.dsl.configureNexus
77
import aws.sdk.kotlin.gradle.util.typedProp
8+
import org.jsoup.Jsoup
89
import java.net.URL
910

1011
buildscript {
@@ -14,6 +15,7 @@ buildscript {
1415
classpath(libs.kotlinx.atomicfu.plugin)
1516
// Add our custom gradle build logic to buildscript classpath
1617
classpath(libs.aws.kotlin.repo.tools.build.support)
18+
classpath(libs.jsoup)
1719
}
1820
}
1921

@@ -54,7 +56,8 @@ allprojects {
5456
"customAssets": [
5557
"${rootProject.file("docs/dokka-presets/assets/logo-icon.svg")}",
5658
"${rootProject.file("docs/dokka-presets/assets/aws_logo_white_59x35.png")}",
57-
"${rootProject.file("docs/dokka-presets/scripts/accessibility.js")}"
59+
"${rootProject.file("docs/dokka-presets/scripts/accessibility.js")}",
60+
"${rootProject.file("docs/dokka-presets/scripts/custom-navigation-loader.js")}"
5861
],
5962
"footerMessage": "© $year, Amazon Web Services, Inc. or its affiliates. All rights reserved.",
6063
"separateInheritedMembers" : true,
@@ -119,6 +122,83 @@ allprojects {
119122
}
120123

121124
project.afterEvaluate {
125+
val trimNavigations = tasks.register("trimNavigations") {
126+
description = "Trims navigation files to remove unrelated child submenu"
127+
group = "documentation"
128+
129+
doLast {
130+
val dokkaOutputDir = rootProject.buildDir.resolve("dokka/htmlMultiModule")
131+
132+
if (!dokkaOutputDir.exists()) {
133+
logger.warn("Dokka output directory not found: ${dokkaOutputDir.absolutePath}")
134+
logger.warn("Skipping navigation trimming")
135+
return@doLast
136+
}
137+
138+
dokkaOutputDir.listFiles { file ->
139+
file.isDirectory && file.resolve("navigation.html").exists()
140+
}?.forEach { moduleDir ->
141+
val moduleName = moduleDir.name
142+
143+
val navFile = File(moduleDir, "navigation.html")
144+
145+
if (navFile.exists()) {
146+
val doc = Jsoup.parse(navFile, "UTF-8")
147+
148+
// Fix navigation links
149+
doc.select("a[href^='../']").forEach { anchor ->
150+
val originalHref = anchor.attr("href")
151+
val trimmedHref = originalHref.replace("../", "")
152+
anchor.attr("href", trimmedHref)
153+
}
154+
155+
val sideMenuParts = doc.select("div.sideMenu > div.sideMenuPart")
156+
157+
sideMenuParts.forEach { submenu ->
158+
val submenuId = submenu.id()
159+
// If this is not the current module's submenu, remove all its nested content
160+
if (submenuId != "$moduleName-nav-submenu") {
161+
val overviewDiv = submenu.select("> div.overview").first()
162+
overviewDiv?.select("span.navButton")?.remove()
163+
submenu.children().remove()
164+
if (overviewDiv != null) {
165+
submenu.appendChild(overviewDiv)
166+
}
167+
}
168+
}
169+
170+
val wrappedContent = "<div class=\"sideMenu\">\n${sideMenuParts.outerHtml()}\n</div>"
171+
navFile.writeText(wrappedContent)
172+
}
173+
}
174+
}
175+
}
176+
177+
val useCustomNavigations = tasks.register("useCustomNavigations") {
178+
group = "documentation"
179+
description = "Replace default Dokka navigation-loader.js with custom implementation"
180+
181+
doLast {
182+
val dokkaOutputDir = rootProject.buildDir.resolve("dokka/htmlMultiModule")
183+
184+
if (!dokkaOutputDir.exists()) {
185+
logger.warn("Dokka output directory not found: ${dokkaOutputDir.absolutePath}")
186+
logger.warn("Skipping using custom navigations")
187+
return@doLast
188+
}
189+
190+
dokkaOutputDir.walkTopDown()
191+
.filter { it.isFile && it.name.endsWith(".html") }
192+
.forEach { file ->
193+
val updatedContent = file.readLines().filterNot { line ->
194+
line.contains("""scripts/navigation-loader.js""")
195+
}.joinToString("\n")
196+
197+
file.writeText(updatedContent)
198+
}
199+
}
200+
}
201+
122202
// configure the root multimodule docs
123203
tasks.dokkaHtmlMultiModule.configure {
124204
moduleName.set("AWS SDK for Kotlin")
@@ -135,6 +215,8 @@ project.afterEvaluate {
135215
// NOTE: these get concatenated
136216
rootProject.file("docs/dokka-presets/README.md"),
137217
)
218+
219+
finalizedBy(trimNavigations, useCustomNavigations)
138220
}
139221
}
140222

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Extracts the module name from a given URL href
2+
function extractModuleName(href) {
3+
try{
4+
const url = new URL(href, window.location.origin);
5+
const pathname = url.pathname;
6+
const pathSegments = pathname.split('/').filter(Boolean);
7+
8+
// For local hosting
9+
if (url.hostname === 'localhost') {
10+
return pathSegments.length >= 1 ? pathSegments[0] : null;
11+
}
12+
13+
return pathSegments.length >= 4 ? pathSegments[3] : null;
14+
}
15+
catch (error) {
16+
return null;
17+
}
18+
}
19+
20+
// Hides the sidebar and adjusts main content layout
21+
function hideSidebar() {
22+
const sidebar = document.getElementById('leftColumn');
23+
const main = document.getElementById('main');
24+
25+
if (sidebar) {
26+
sidebar.style.display = 'none';
27+
}
28+
29+
if (main) {
30+
main.style.marginLeft = '0';
31+
main.style.width = '100%';
32+
}
33+
}
34+
35+
function loadNavigation() {
36+
const moduleName = extractModuleName(window.location.href);
37+
38+
// Hide sidebar for root index page
39+
if (moduleName === "index.html") {
40+
hideSidebar()
41+
return Promise.resolve('');
42+
}
43+
44+
const navigationPath = moduleName
45+
? `${pathToRoot}${moduleName}/navigation.html`
46+
: `${pathToRoot}navigation.html`;
47+
48+
return fetch(navigationPath)
49+
.then(response => response.text())
50+
.catch(error => {
51+
// Use root navigation as a fallback
52+
return fetch(pathToRoot + "navigation.html")
53+
.then(response => response.text());
54+
});
55+
}
56+
57+
navigationPageText = loadNavigation()
58+
// navigationPageText = fetch(pathToRoot + extractModuleName() + "/navigation.html").then(response => response.text())
59+
60+
displayNavigationFromPage = () => {
61+
navigationPageText.then(data => {
62+
document.getElementById("sideMenu").innerHTML = data;
63+
}).then(() => {
64+
document.querySelectorAll(".overview > a").forEach(link => {
65+
link.setAttribute("href", pathToRoot + link.getAttribute("href"));
66+
})
67+
}).then(() => {
68+
document.querySelectorAll(".sideMenuPart").forEach(nav => {
69+
if (!nav.classList.contains("hidden"))
70+
nav.classList.add("hidden")
71+
})
72+
}).then(() => {
73+
revealNavigationForCurrentPage()
74+
}).then(() => {
75+
scrollNavigationToSelectedElement()
76+
})
77+
document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => {
78+
anchor.addEventListener('click', function (e) {
79+
e.preventDefault();
80+
document.querySelector(this.getAttribute('href')).scrollIntoView({
81+
behavior: 'smooth'
82+
});
83+
});
84+
});
85+
}
86+
87+
revealNavigationForCurrentPage = () => {
88+
let pageId = document.getElementById("content").attributes["pageIds"].value.toString();
89+
let parts = document.querySelectorAll(".sideMenuPart");
90+
let found = 0;
91+
do {
92+
parts.forEach(part => {
93+
if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) {
94+
found = 1;
95+
if (part.classList.contains("hidden")) {
96+
part.classList.remove("hidden");
97+
part.setAttribute('data-active', "");
98+
}
99+
revealParents(part)
100+
}
101+
});
102+
pageId = pageId.substring(0, pageId.lastIndexOf("/"))
103+
} while (pageId.indexOf("/") !== -1 && found === 0)
104+
};
105+
revealParents = (part) => {
106+
if (part.classList.contains("sideMenuPart")) {
107+
if (part.classList.contains("hidden"))
108+
part.classList.remove("hidden");
109+
revealParents(part.parentNode)
110+
}
111+
};
112+
113+
scrollNavigationToSelectedElement = () => {
114+
let selectedElement = document.querySelector('div.sideMenuPart[data-active]')
115+
if (selectedElement == null) { // nothing selected, probably just the main page opened
116+
return
117+
}
118+
119+
let hasIcon = selectedElement.querySelectorAll(":scope > div.overview span.nav-icon").length > 0
120+
121+
// for instance enums also have children and are expandable, but are not package/module elements
122+
let isPackageElement = selectedElement.children.length > 1 && !hasIcon
123+
if (isPackageElement) {
124+
// if package is selected or linked, it makes sense to align it to top
125+
// so that you can see all the members it contains
126+
selectedElement.scrollIntoView(true)
127+
} else {
128+
// if a member within a package is linked, it makes sense to center it since it,
129+
// this should make it easier to look at surrounding members
130+
selectedElement.scrollIntoView({
131+
behavior: 'auto',
132+
block: 'center',
133+
inline: 'center'
134+
})
135+
}
136+
}
137+
138+
/*
139+
This is a work-around for safari being IE of our times.
140+
It doesn't fire a DOMContentLoaded, presumabely because eventListener is added after it wants to do it
141+
*/
142+
if (document.readyState == 'loading') {
143+
window.addEventListener('DOMContentLoaded', () => {
144+
displayNavigationFromPage()
145+
})
146+
} else {
147+
displayNavigationFromPage()
148+
}

0 commit comments

Comments
 (0)