Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
84 changes: 83 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import aws.sdk.kotlin.gradle.dsl.configureLinting
import aws.sdk.kotlin.gradle.dsl.configureNexus
import aws.sdk.kotlin.gradle.util.typedProp
import org.jsoup.Jsoup
import java.net.URL

buildscript {
Expand All @@ -14,6 +15,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 Down Expand Up @@ -54,7 +56,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 @@ -119,6 +122,83 @@ allprojects {
}

project.afterEvaluate {
val trimNavigations = 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.warn("Dokka output directory not found: ${dokkaOutputDir.absolutePath}")
logger.warn("Skipping navigation trimming")
return@doLast
}

dokkaOutputDir.listFiles { file ->
file.isDirectory && file.resolve("navigation.html").exists()
}?.forEach { moduleDir ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should only be one navigation.html file (in the root of dokkaOutputDir) since we prune the duplicate navigation.html files during our build.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still want to prune them? Now we are trimming and using these navigations

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize your navigation loader would use each module's navigation.html. Ok, that will work but we'll need to update our build process to not remove duplicate files (and make sure the docs artifact size doesn't increase significantly)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll remove that. I did a quick estimation, the doc size should only increased by 300 - 400 MB

val moduleName = moduleDir.name

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

if (navFile.exists()) {
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 ->
val submenuId = submenu.id()
// If this is not the current module's submenu, remove all its nested content
if (submenuId != "$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)
}
}
}
}

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

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

if (!dokkaOutputDir.exists()) {
logger.warn("Dokka output directory not found: ${dokkaOutputDir.absolutePath}")
logger.warn("Skipping using custom navigations")
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)
}
}
}

// configure the root multimodule docs
tasks.dokkaHtmlMultiModule.configure {
moduleName.set("AWS SDK for Kotlin")
Expand All @@ -135,6 +215,8 @@ project.afterEvaluate {
// NOTE: these get concatenated
rootProject.file("docs/dokka-presets/README.md"),
)

finalizedBy(trimNavigations, useCustomNavigations)
}
}

Expand Down
147 changes: 147 additions & 0 deletions docs/dokka-presets/scripts/custom-navigation-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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()

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