Files
google__adk-docs/docs/api-reference/kotlin/scripts/platform-content-handler.js
Shahin Saadati 4505cb7e13 Fix the Kotlin API reference generator for Dokka 2 and regenerate at 0.9.0 (#2204)
## Summary

The published Kotlin API reference has rendered **0.5.0** since it was
last
generated — four releases behind `main`, which is on 0.9.0. This
regenerates it
and fixes the generator that made it impossible.

It is not a forgotten manual step. `tools/kotlin-api-docs/generate.sh`
cannot
run against any adk-kotlin newer than **v0.6.0**.

## Why it was stuck

adk-kotlin moved from Dokka **1.9.20** to **2.2.0** at **v0.7.0**, and
Dokka 2
changed three things the script depends on:

1. **The task is gone.** `dokkaHtmlMultiModule` survives only as a
disabled
stub. Against a v0.9.0 clone, `./gradlew tasks --all` lists it verbatim
as:
   ```
   dokkaHtmlMultiModule - [⚠ V1 tasks disabled] Runs all subprojects …
   ```
   The build fails before generating anything.
2. **The output path moved** from `build/dokka/htmlMultiModule` to
   `build/dokka/html`.
3. **Root aggregation is gone.** Dokka 1 inferred the unified
multi-module site
   from the subprojects; Dokka 2 requires an explicit
`dependencies { dokka(project(...)) }` block, and adk-kotlin's root
build has
none. So even with the task name fixed there is no combined site to copy
—
   only a dozen disconnected per-module ones.

## What this changes

**`tools/kotlin-api-docs/generate.sh`** — the three fixes above, plus a
post-generation check that `index.html` actually renders the requested
version.
Nothing verified that before, which is exactly how a 0.5.0 site sat in
the repo
looking freshly built.

The aggregation block is injected into the throwaway clone the script
already
makes, rather than sent upstream to adk-kotlin. That keeps the whole fix
inside
adk-docs — no second repo, no second review — and makes the module list
a docs
decision rather than an SDK one.

**`docs/api-reference/kotlin/`** — regenerated at 0.9.0. 2,462 files.

## Module coverage changes, and not purely additively

Please review this part specifically; it is the only judgement call
here.

| Module | Before | After | |
|---|---|---|---|
| `core`, `a2a`, `litertlm`, `processor`, `webserver` | ✅ | ✅ |
unchanged |
| `integrations` | ❌ | ✅ | **added** |
| `testing` | ❌ | ✅ | **added** |
| `examples` | ✅ | ❌ | **dropped** |
| `firebase`, `mlkit` | ❌ | ❌ | attempted, produce nothing |

- **`integrations` matters most.** It hosts
`BigQueryAgentAnalyticsPlugin`,
which `docs/integrations/bigquery-agent-analytics.md` documents and the
API
  reference has never covered.
- **`examples` is dropped** because it is sample code rather than API
surface,
and it declares a JDK 21 toolchain that fails to auto-provision and
takes the
entire build down with it. Happy to restore it if you disagree, but it
needs
  the toolchain problem solved first.
- **`firebase` and `mlkit` aggregate but emit empty directories** —
Dokka 2
generates no pages for their androidMain source sets. I left them out
rather
than shipping empty modules that imply coverage that is not there.
Making them
work needs a Dokka source-set fix in adk-kotlin, so it is out of scope
here.

## Verification

- A clean run of the committed script reproduces this exact tree.
- `index.html` renders `0.9.0`, and no file under
`docs/api-reference/kotlin/`
  still contains `0.5.0`.
- The one deep link into the reference —
`docs/runtime/runconfig.md:364`, into
  `google-adk-kotlin-core/com.google.adk.kt.agents/-run-config/` — still
  resolves.
- The GA tag is injected exactly once per page. Eight files are skipped:
the
  `navigation.html` fragments, which have no `<head>` to inject into.
- No temp-clone paths leaked into the generated HTML.

Built with **JDK 26** and Android SDK **platform 34**. adk-kotlin
declares a JDK
17 toolchain, but Dokka never needed to launch it for the aggregated
modules.

## Reviewing 2,462 files

Almost all of it is generated HTML. The only hand-written change is
`tools/kotlin-api-docs/generate.sh` (+57/-13); everything else is Dokka
output.
Reviewing the script and spot-checking a couple of rendered pages is the
useful
version of this review.

## Relationship to #2152

#2152 pins adk-docs to adk-kotlin 1.0.0 and lists regenerating this
reference on
its pre-merge checklist, blocked on a `v1.0.0` tag that does not exist
yet.

This PR deliberately does **not** wait for that. Doing it at 0.9.0 now
clears
four releases of staleness immediately and proves the toolchain works
while
there is no deadline, instead of discovering the generator is broken on
release
day. Once 1.0.0 ships, #2152 re-runs the same script with a different
argument.
2026-09-08 13:10:59 -07:00

311 lines
11 KiB
JavaScript

/*
* Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
filteringContext = {
dependencies: {},
restrictedDependencies: [],
activeFilters: []
}
let highlightedAnchor;
let topNavbarOffset;
let sourcesetNotification;
window.addEventListener('load', () => {
document.querySelectorAll("div[data-platform-hinted]")
.forEach(elem => elem.addEventListener('click', (event) => togglePlatformDependent(event, elem)))
const filterSection = document.getElementById('filter-section')
if (filterSection) {
filterSection.addEventListener('click', (event) => filterButtonHandler(event))
initializeFiltering()
}
if (typeof initTabs === 'function') {
initTabs() // initTabs comes from ui-kit/tabs
}
handleAnchor()
topNavbarOffset = document.getElementById('navigation-wrapper')
darkModeSwitch()
})
const darkModeSwitch = () => {
const localStorageKey = "dokka-dark-mode"
const storage = safeLocalStorage.getItem(localStorageKey)
const osDarkSchemePreferred = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const darkModeEnabled = storage ? JSON.parse(storage) : osDarkSchemePreferred
const element = document.getElementById("theme-toggle-button")
// Notify external scripts about changing dark mode, runnable samples plugin depends on this
if (window.onDarkModeChanged) {
window.onDarkModeChanged(darkModeEnabled)
}
element.addEventListener('click', () => {
const enabledClasses = document.getElementsByTagName("html")[0].classList
enabledClasses.toggle("theme-dark")
//if previously we had saved dark theme then we set it to light as this is what we save in local storage
const darkModeEnabled = enabledClasses.contains("theme-dark")
// Notify external scripts about changing dark mode, runnable samples plugin depends on this
if (window.onDarkModeChanged) {
window.onDarkModeChanged(darkModeEnabled)
}
safeLocalStorage.setItem(localStorageKey, JSON.stringify(darkModeEnabled))
})
}
// Hash change is needed in order to allow for linking inside the same page with anchors
// If this is not present user is forced to refresh the site in order to use an anchor
window.onhashchange = handleAnchor
function scrollToElementInContent(element) {
const scrollToElement = () => document.getElementById('main').scrollTo({
top: element.offsetTop - topNavbarOffset.offsetHeight,
behavior: "smooth"
})
const waitAndScroll = () => {
setTimeout(() => {
if (topNavbarOffset) {
scrollToElement()
} else {
waitForScroll()
}
}, 50)
}
if (topNavbarOffset) {
scrollToElement()
} else {
waitAndScroll()
}
}
function handleAnchor() {
if (highlightedAnchor) {
highlightedAnchor.classList.remove('anchor-highlight')
highlightedAnchor = null;
}
let searchForContentTarget = function (element) {
if (element && element.hasAttribute) {
if (element.hasAttribute("data-togglable")) return element.getAttribute("data-togglable");
else return searchForContentTarget(element.parentNode)
} else return null
}
let findAnyTab = function (target) {
let result = null
document.querySelectorAll('div[tabs-section] > button[data-togglable]')
.forEach(node => {
if(node.getAttribute("data-togglable").split(",").includes(target)) {
result = node
}
})
return result
}
let anchor = window.location.hash
if (anchor !== "") {
anchor = anchor.substring(1)
let element = document.querySelector('a[data-name="' + anchor + '"]')
if (element) {
const content = element.nextElementSibling
const contentStyle = window.getComputedStyle(content)
if(contentStyle.display === 'none') {
let tab = findAnyTab(searchForContentTarget(content))
if (tab) {
toggleSections(tab) // toggleSections comes from ui-kit/tabs
}
}
if (content) {
content.classList.add('anchor-highlight')
highlightedAnchor = content
}
scrollToElementInContent(element)
}
}
}
function filterButtonHandler(event) {
if (event.target.tagName === "BUTTON" && event.target.hasAttribute("data-filter")) {
let sourceset = event.target.getAttribute("data-filter")
if (filteringContext.activeFilters.indexOf(sourceset) !== -1) {
filterSourceset(sourceset)
} else {
unfilterSourceset(sourceset)
}
}
}
function initializeFiltering() {
filteringContext.dependencies = JSON.parse(sourceset_dependencies)
document.querySelectorAll("#filter-section > button")
.forEach(p => filteringContext.restrictedDependencies.push(p.getAttribute("data-filter")))
Object.keys(filteringContext.dependencies).forEach(p => {
filteringContext.dependencies[p] = filteringContext.dependencies[p]
.filter(q => -1 !== filteringContext.restrictedDependencies.indexOf(q))
})
let cached = safeLocalStorage.getItem('inactive-filters')
if (cached) {
let parsed = JSON.parse(cached)
filteringContext.activeFilters = filteringContext.restrictedDependencies
.filter(q => parsed.indexOf(q) === -1)
} else {
filteringContext.activeFilters = filteringContext.restrictedDependencies
}
refreshFiltering()
}
function filterSourceset(sourceset) {
filteringContext.activeFilters = filteringContext.activeFilters.filter(p => p !== sourceset)
refreshFiltering()
addSourcesetFilterToCache(sourceset)
}
function unfilterSourceset(sourceset) {
if (filteringContext.activeFilters.length === 0) {
filteringContext.activeFilters = filteringContext.dependencies[sourceset].concat([sourceset])
refreshFiltering()
filteringContext.dependencies[sourceset].concat([sourceset]).forEach(p => removeSourcesetFilterFromCache(p))
} else {
filteringContext.activeFilters.push(sourceset)
refreshFiltering()
removeSourcesetFilterFromCache(sourceset)
}
}
function addSourcesetFilterToCache(sourceset) {
let cached = safeLocalStorage.getItem('inactive-filters')
if (cached) {
let parsed = JSON.parse(cached)
safeLocalStorage.setItem('inactive-filters', JSON.stringify(parsed.concat([sourceset])))
} else {
safeLocalStorage.setItem('inactive-filters', JSON.stringify([sourceset]))
}
}
function removeSourcesetFilterFromCache(sourceset) {
let cached = safeLocalStorage.getItem('inactive-filters')
if (cached) {
let parsed = JSON.parse(cached)
safeLocalStorage.setItem('inactive-filters', JSON.stringify(parsed.filter(p => p !== sourceset)))
}
}
function refreshSourcesetsCache() {
safeLocalStorage.setItem('inactive-filters', JSON.stringify(filteringContext.restrictedDependencies.filter(p => -1 === filteringContext.activeFilters.indexOf(p))))
}
function togglePlatformDependent(e, container) {
let target = e.target
if (target.tagName !== 'BUTTON') return;
let index = target.getAttribute('data-toggle')
for (let child of container.children) {
if (child.hasAttribute('data-toggle-list')) {
for (let bm of child.children) {
if (bm === target) {
bm.setAttribute('data-active', "")
bm.setAttribute('aria-pressed', "true")
} else if (bm !== target) {
bm.removeAttribute('data-active')
bm.removeAttribute('aria-pressed')
}
}
} else if (child.getAttribute('data-togglable') === index) {
child.setAttribute('data-active', "")
child.setAttribute('aria-pressed', "true")
} else {
child.removeAttribute('data-active')
child.removeAttribute('aria-pressed')
}
}
}
function refreshFiltering() {
let sourcesetList = filteringContext.activeFilters
document.querySelectorAll("[data-filterable-set]")
.forEach(
elem => {
let platformList = elem.getAttribute("data-filterable-set").split(',').filter(v => -1 !== sourcesetList.indexOf(v))
elem.setAttribute("data-filterable-current", platformList.join(','))
}
)
refreshFilterButtons()
refreshPlatformTabs()
refreshNoContentNotification()
}
function refreshNoContentNotification() {
const element = document.getElementsByClassName("main-content")[0]
const filteredMessage = document.querySelector(".filtered-message")
if(filteringContext.activeFilters.length === 0){
element.style.display = "none";
if (!filteredMessage) {
const appended = document.createElement("div")
appended.className = "filtered-message"
appended.innerText = "All documentation is filtered, please adjust your source set filters in top-right corner of the screen"
sourcesetNotification = appended
element.parentNode.prepend(appended)
}
} else {
if(sourcesetNotification) sourcesetNotification.remove()
element.style.display = "block"
}
}
function refreshPlatformTabs() {
document.querySelectorAll(".platform-hinted > .platform-bookmarks-row").forEach(
p => {
let active = false;
let firstAvailable = null
p.childNodes.forEach(
element => {
if (element.getAttribute("data-filterable-current") !== '') {
if (firstAvailable === null) {
firstAvailable = element
}
if (element.hasAttribute("data-active")) {
active = true;
}
}
}
)
if (active === false && firstAvailable) {
firstAvailable.click()
}
}
)
}
function refreshFilterButtons() {
document.querySelectorAll("#filter-section > button")
.forEach(f => {
if (filteringContext.activeFilters.indexOf(f.getAttribute("data-filter")) !== -1) {
f.setAttribute("data-active", "")
f.setAttribute("aria-pressed", "true")
} else {
f.removeAttribute("data-active")
f.removeAttribute("aria-pressed")
}
})
document.querySelectorAll("#filter-section .checkbox--input")
.forEach(f => {
const isChecked = filteringContext.activeFilters.indexOf(f.getAttribute("data-filter")) !== -1
f.checked = isChecked;
if (isChecked) {
f.setAttribute("aria-pressed", "true")
} else {
f.removeAttribute("aria-pressed");
}
})
}