-
Notifications
You must be signed in to change notification settings - Fork 55
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
Changes from 2 commits
08bce21
f5656ef
ab4bc94
ea30f9e
c3a551c
07af478
d9bac19
2b187bc
eea82d4
63311f5
398c877
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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, | ||
|
|
@@ -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 -> | ||
|
||
| val moduleName = moduleDir.name | ||
|
|
||
| val navFile = File(moduleDir, "navigation.html") | ||
|
|
||
| if (navFile.exists()) { | ||
xinsong-cui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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") { | ||
xinsong-cui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| } | ||
| } | ||
xinsong-cui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // configure the root multimodule docs | ||
| tasks.dokkaHtmlMultiModule.configure { | ||
| moduleName.set("AWS SDK for Kotlin") | ||
|
|
@@ -135,6 +215,8 @@ project.afterEvaluate { | |
| // NOTE: these get concatenated | ||
| rootProject.file("docs/dokka-presets/README.md"), | ||
| ) | ||
|
|
||
| finalizedBy(trimNavigations, useCustomNavigations) | ||
xinsong-cui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
|
|
||
xinsong-cui marked this conversation as resolved.
Show resolved
Hide resolved
|
| 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`; | ||
xinsong-cui marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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() | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.