Skip to content

feat: api ref doc sidebar optimization #1599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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