Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/3828400d-3899-4d31-9db7-0b973ce10e43.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "3828400d-3899-4d31-9db7-0b973ce10e43",
"type": "feature",
"description": "Optimize Kotlin API Reference documentation navigation sidebar"
}
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ buildscript {
classpath(libs.kotlinx.atomicfu.plugin)
// Add our custom gradle build logic to buildscript classpath
classpath(libs.aws.kotlin.repo.tools.build.support)
classpath(libs.jsoup)
}
}

Expand All @@ -39,6 +40,8 @@ val testJavaVersion = typedProp<String>("test.java.version")?.let {
}

allprojects {
apply(from = "${rootProject.file("buildSrc/src/main/kotlin/dokka-customization.gradle.kts")}")

tasks.withType<org.jetbrains.dokka.gradle.AbstractDokkaTask>().configureEach {
val sdkVersion: String by project
moduleVersion.set(sdkVersion)
Expand All @@ -54,7 +57,8 @@ allprojects {
"customAssets": [
"${rootProject.file("docs/dokka-presets/assets/logo-icon.svg")}",
"${rootProject.file("docs/dokka-presets/assets/aws_logo_white_59x35.png")}",
"${rootProject.file("docs/dokka-presets/scripts/accessibility.js")}"
"${rootProject.file("docs/dokka-presets/scripts/accessibility.js")}",
"${rootProject.file("docs/dokka-presets/scripts/custom-navigation-loader.js")}"
],
"footerMessage": "© $year, Amazon Web Services, Inc. or its affiliates. All rights reserved.",
"separateInheritedMembers" : true,
Expand Down Expand Up @@ -135,6 +139,8 @@ project.afterEvaluate {
// NOTE: these get concatenated
rootProject.file("docs/dokka-presets/README.md"),
)

finalizedBy("trimNavigations", "applyCustomNavigationLoader")
}
}

Expand Down
4 changes: 4 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ plugins {
repositories {
mavenCentral()
}

dependencies {
implementation(libs.jsoup)
}
73 changes: 73 additions & 0 deletions buildSrc/src/main/kotlin/dokka-customization.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import org.jsoup.Jsoup

tasks.register("trimNavigations") {
description = "Trims navigation files to remove unrelated child submenu"
group = "documentation"

doLast {
val dokkaOutputDir = rootProject.buildDir.resolve("dokka/htmlMultiModule")

if (!dokkaOutputDir.exists()) {
logger.info("Dokka output directory not found at ${dokkaOutputDir.absolutePath}, skipping navigation trimming")
return@doLast
}

dokkaOutputDir.listFiles { file ->
file.isDirectory && file.resolve("navigation.html").exists()
}?.forEach { moduleDir ->
val moduleName = moduleDir.name

val navFile = File(moduleDir, "navigation.html")

val doc = Jsoup.parse(navFile, "UTF-8")

// Fix navigation links
doc.select("a[href^='../']").forEach { anchor ->
val originalHref = anchor.attr("href")
val trimmedHref = originalHref.replace("../", "")
anchor.attr("href", trimmedHref)
}

val sideMenuParts = doc.select("div.sideMenu > div.sideMenuPart")

sideMenuParts.forEach { submenu ->
// If this is not the current module's submenu, remove all its nested content
if (submenu.id() != "$moduleName-nav-submenu") {
val overviewDiv = submenu.select("> div.overview").first()
overviewDiv?.select("span.navButton")?.remove()
submenu.children().remove()
if (overviewDiv != null) {
submenu.appendChild(overviewDiv)
}
}
}

val wrappedContent = "<div class=\"sideMenu\">\n${sideMenuParts.outerHtml()}\n</div>"
navFile.writeText(wrappedContent)
}
}
}

tasks.register("applyCustomNavigationLoader") {
group = "documentation"
description = "Replace default Dokka navigation-loader.js with custom implementation"

doLast {
val dokkaOutputDir = rootProject.buildDir.resolve("dokka/htmlMultiModule")

if (!dokkaOutputDir.exists()) {
logger.info("Dokka output directory not found at ${dokkaOutputDir.absolutePath}, skipping apply custom navigation loader")
return@doLast
}

dokkaOutputDir.walkTopDown()
.filter { it.isFile && it.name.endsWith(".html") }
.forEach { file ->
val updatedContent = file.readLines().filterNot { line ->
line.contains("""scripts/navigation-loader.js""")
}.joinToString("\n")

file.writeText(updatedContent)
}
}
}
152 changes: 152 additions & 0 deletions docs/dokka-presets/scripts/custom-navigation-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Custom navigation loader for AWS SDK for Kotlin documentation.

// Extracts the module name from a given URL href
function extractModuleName(href) {
try{
const url = new URL(href, window.location.origin);
const pathname = url.pathname;
const pathSegments = pathname.split('/').filter(Boolean);

// For local hosting
if (url.hostname === 'localhost') {
return pathSegments.length >= 1 ? pathSegments[0] : null;
}

return pathSegments.length >= 4 ? pathSegments[3] : null;
}
catch (error) {
return null;
}
}

// Hides the sidebar and adjusts main content layout
function hideSidebar() {
const sidebar = document.getElementById('leftColumn');
const main = document.getElementById('main');

if (sidebar) {
sidebar.style.display = 'none';
}

if (main) {
main.style.marginLeft = '0';
main.style.width = '100%';
}
}

function loadNavigation() {
const moduleName = extractModuleName(window.location.href);

// Hide sidebar for root index page
if (moduleName === "index.html") {
hideSidebar()
return Promise.resolve('');
}

const navigationPath = moduleName
? `${pathToRoot}${moduleName}/navigation.html`
: `${pathToRoot}navigation.html`;

return fetch(navigationPath)
.then(response => response.text())
.catch(error => {
// Use root navigation as a fallback
return fetch(pathToRoot + "navigation.html")
.then(response => response.text());
});
}

navigationPageText = loadNavigation()

// =================================================================
// Everything below this is copied from Dokka's navigation-loader.js
// =================================================================
displayNavigationFromPage = () => {
navigationPageText.then(data => {
document.getElementById("sideMenu").innerHTML = data;
}).then(() => {
document.querySelectorAll(".overview > a").forEach(link => {
link.setAttribute("href", pathToRoot + link.getAttribute("href"));
})
}).then(() => {
document.querySelectorAll(".sideMenuPart").forEach(nav => {
if (!nav.classList.contains("hidden"))
nav.classList.add("hidden")
})
}).then(() => {
revealNavigationForCurrentPage()
}).then(() => {
scrollNavigationToSelectedElement()
})
document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
document.querySelector(this.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});
}

revealNavigationForCurrentPage = () => {
let pageId = document.getElementById("content").attributes["pageIds"].value.toString();
let parts = document.querySelectorAll(".sideMenuPart");
let found = 0;
do {
parts.forEach(part => {
if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) {
found = 1;
if (part.classList.contains("hidden")) {
part.classList.remove("hidden");
part.setAttribute('data-active', "");
}
revealParents(part)
}
});
pageId = pageId.substring(0, pageId.lastIndexOf("/"))
} while (pageId.indexOf("/") !== -1 && found === 0)
};
revealParents = (part) => {
if (part.classList.contains("sideMenuPart")) {
if (part.classList.contains("hidden"))
part.classList.remove("hidden");
revealParents(part.parentNode)
}
};

scrollNavigationToSelectedElement = () => {
let selectedElement = document.querySelector('div.sideMenuPart[data-active]')
if (selectedElement == null) { // nothing selected, probably just the main page opened
return
}

let hasIcon = selectedElement.querySelectorAll(":scope > div.overview span.nav-icon").length > 0

// for instance enums also have children and are expandable, but are not package/module elements
let isPackageElement = selectedElement.children.length > 1 && !hasIcon
if (isPackageElement) {
// if package is selected or linked, it makes sense to align it to top
// so that you can see all the members it contains
selectedElement.scrollIntoView(true)
} else {
// if a member within a package is linked, it makes sense to center it since it,
// this should make it easier to look at surrounding members
selectedElement.scrollIntoView({
behavior: 'auto',
block: 'center',
inline: 'center'
})
}
}

/*
This is a work-around for safari being IE of our times.
It doesn't fire a DOMContentLoaded, presumabely because eventListener is added after it wants to do it
*/
if (document.readyState == 'loading') {
window.addEventListener('DOMContentLoaded', () => {
displayNavigationFromPage()
})
} else {
displayNavigationFromPage()
}
Loading