blob: 4fca451cc04ee05feae3946245f7be82f9640391 [file] [log] [blame] [edit]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"use strict";
let tocAnchors = [];
let lastMouseOffY = 0;
let onloadFired = false;
const postLoadActions = [];
let tocEventHandlersInstalled = false;
let resizeObserver = undefined;
function doAfterLoadEvent(action) {
if (onloadFired) {
return action();
}
postLoadActions.push(action);
}
function setupSandwichMenu() {
const header = document.querySelector(".site-header");
const docsNav = document.querySelector(".nav");
const menu = header.querySelector(".menu");
menu.addEventListener("click", (e) => {
e.preventDefault();
// If we are displaying any /docs, toggle the navbar instead (the TOC).
if (docsNav) {
// |after_first_click| is to avoid spurious transitions on page load.
docsNav.classList.add("after_first_click");
updateNav();
setTimeout(() => docsNav.classList.toggle("expanded"), 0);
} else {
header.classList.toggle("expanded");
}
});
}
// (Re-)Generates the Table Of Contents for docs (the right-hand-side one).
function updateTOC() {
const tocContainer = document.querySelector(".docs .toc");
if (!tocContainer) return;
const toc = document.createElement("ul");
const anchors = document.querySelectorAll(".doc a.anchor");
tocAnchors = [];
for (const anchor of anchors) {
const li = document.createElement("li");
const link = document.createElement("a");
link.innerText = anchor.parentElement.innerText;
link.href = anchor.href;
link.onclick = () => {
onScroll(link);
};
li.appendChild(link);
if (anchor.parentElement.tagName === "H3") li.style.paddingLeft = "10px";
toc.appendChild(li);
doAfterLoadEvent(() => {
tocAnchors.push({
top: anchor.offsetTop + anchor.offsetHeight / 2,
obj: link,
});
});
}
tocContainer.innerHTML = "";
tocContainer.appendChild(toc);
// Add event handlers on the first call (can be called more than once to
// recompute anchors on resize).
if (tocEventHandlersInstalled) return;
tocEventHandlersInstalled = true;
const doc = document.querySelector(".doc");
const passive = { passive: true };
if (doc) {
const offY = doc.offsetTop;
doc.addEventListener("mousemove", (e) => onMouseMove(offY, e), passive);
doc.addEventListener(
"mouseleave",
() => {
lastMouseOffY = 0;
},
passive,
);
}
window.addEventListener("scroll", () => onScroll(), passive);
resizeObserver = new ResizeObserver(() =>
requestAnimationFrame(() => {
updateNav();
updateTOC();
}),
);
resizeObserver.observe(doc);
}
// Highlights the current TOC anchor depending on the scroll offset.
function onMouseMove(offY, e) {
lastMouseOffY = e.clientY - offY;
onScroll();
}
function onScroll(forceHighlight) {
const y = document.documentElement.scrollTop + lastMouseOffY;
let highEl = undefined;
for (const x of tocAnchors) {
if (y < x.top) continue;
highEl = x.obj;
}
for (const link of document.querySelectorAll(".docs .toc a")) {
if ((!forceHighlight && link === highEl) || forceHighlight === link) {
link.classList.add("highlighted");
} else {
link.classList.remove("highlighted");
}
}
}
// This function needs to be idempotent as it is called more than once (on every
// resize).
function updateNav() {
const curDoc = document.querySelector(".doc");
let curFileName = "";
if (curDoc) curFileName = curDoc.dataset["mdFile"];
// First identify all the top-level nav entries (Quickstart, Data Sources,
// ...) and make them compressible.
const toplevelSections = document.querySelectorAll(".docs .nav > ul > li");
const toplevelLinks = [];
for (const sec of toplevelSections) {
const childMenu = sec.querySelector("ul");
if (!childMenu) {
// Don't make it compressible if it has no children (e.g. the very
// first 'Introduction' link).
continue;
}
// Don't make it compressible if the entry has an actual link (e.g. the very
// first 'Introduction' link), because otherwise it become ambiguous whether
// the link should toggle or open the link.
const link = sec.querySelector("a");
if (!link || !link.href.endsWith("#")) continue;
sec.classList.add("compressible");
// Remember the compressed status as long as the page is opened, so clicking
// through links keeps the sidebar in a consistent visual state.
const memoKey = `docs.nav.compressed[${link.innerHTML}]`;
if (sessionStorage.getItem(memoKey) === "1") {
sec.classList.add("compressed");
}
doAfterLoadEvent(() => {
childMenu.style.maxHeight = `${childMenu.scrollHeight + 40}px`;
});
toplevelLinks.push(link);
link.onclick = (evt) => {
evt.preventDefault();
sec.classList.toggle("compressed");
if (sec.classList.contains("compressed")) {
sessionStorage.setItem(memoKey, "1");
} else {
sessionStorage.removeItem(memoKey);
}
};
}
const exps = document.querySelectorAll(".docs .nav ul a");
let found = false;
for (const x of exps) {
// If the url of the entry matches the url of the page, mark the item as
// highlighted and expand all its parents.
if (!x.href) continue;
const url = new URL(x.href);
if (x.href.endsWith("#")) {
// This is a non-leaf link to a menu.
if (toplevelLinks.indexOf(x) < 0) {
x.removeAttribute("href");
}
} else if (url.pathname === curFileName && !found) {
x.classList.add("selected");
doAfterLoadEvent(() => x.scrollIntoViewIfNeeded());
found = true; // Highlight only the first occurrence.
}
}
}
// If the page contains a ```mermaid ``` block, lazily loads the plugin and
// renders.
function initMermaid() {
const graphs = document.querySelectorAll(".mermaid");
// Skip if there are no mermaid graphs to render.
if (!graphs.length) return;
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "/assets/mermaid.min.js";
const themeCSS = `
.cluster rect { fill: #FCFCFC; stroke: #ddd }
.node rect { fill: #DCEDC8; stroke: #8BC34A}
.edgeLabel:not(:empty) {
border-radius: 6px;
font-size: 0.9em;
padding: 4px;
background: #F5F5F5;
border: 1px solid #DDDDDD;
color: #666;
}
`;
script.addEventListener("load", () => {
mermaid.initialize({
startOnLoad: false,
themeCSS: themeCSS,
securityLevel: "loose", // To allow #in-page-links
});
for (const graph of graphs) {
requestAnimationFrame(() => {
mermaid.init(undefined, graph);
graph.classList.add("rendered");
});
}
});
document.body.appendChild(script);
}
function setupSearch() {
const URL =
"https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q=";
const searchContainer = document.getElementById("search");
const searchBox = document.getElementById("search-box");
const searchRes = document.getElementById("search-res");
if (!searchBox || !searchRes) return;
document.body.addEventListener("keydown", (e) => {
if (e.key === "/" && e.target.tagName.toLowerCase() === "body") {
searchBox.setSelectionRange(0, -1);
searchBox.focus();
e.preventDefault();
} else if (e.key === "Escape" && searchContainer.contains(e.target)) {
searchBox.blur();
// Handle the case of clicking Tab and moving down to results.
e.target.blur();
}
});
let timerId = -1;
let lastSearchId = 0;
const doSearch = async () => {
timerId = -1;
searchRes.style.width = `${searchBox.offsetWidth}px`;
// `searchId` handles the case of two subsequent requests racing. This is to
// prevent older results, delivered in reverse order, to replace newer ones.
const searchId = ++lastSearchId;
const f = await fetch(URL + encodeURIComponent(searchBox.value));
const jsonRes = await f.json();
const results = jsonRes["items"];
searchRes.innerHTML = "";
if (results === undefined || searchId != lastSearchId) {
return;
}
for (const res of results) {
const link = document.createElement("a");
link.href = res.link;
const title = document.createElement("div");
title.className = "sr-title";
title.innerText = res.title.replace(" - Perfetto Tracing Docs", "");
link.appendChild(title);
const snippet = document.createElement("div");
snippet.className = "sr-snippet";
snippet.innerText = res.snippet;
link.appendChild(snippet);
const div = document.createElement("div");
div.appendChild(link);
searchRes.appendChild(div);
}
};
searchBox.addEventListener("keyup", () => {
if (timerId >= 0) return;
timerId = setTimeout(doSearch, 200);
});
}
window.addEventListener("DOMContentLoaded", () => {
updateNav();
updateTOC();
});
window.addEventListener("load", () => {
setupSandwichMenu();
initMermaid();
// Don't smooth-scroll on pages that are too long (e.g. reference pages).
if (document.body.scrollHeight < 10000) {
document.documentElement.style.scrollBehavior = "smooth";
} else {
document.documentElement.style.scrollBehavior = "initial";
}
onloadFired = true;
while (postLoadActions.length > 0) {
postLoadActions.shift()();
}
updateTOC();
setupSearch();
// Enable animations only after the load event. This is to prevent glitches
// when switching pages.
document.documentElement.style.setProperty("--anim-enabled", "1");
});
// Handles redirects from the old docs.perfetto.dev.
const legacyRedirectMap = {
"#/contributing": "/docs/contributing/getting-started#community",
"#/build-instructions": "/docs/contributing/build-instructions",
"#/testing": "/docs/contributing/testing",
"#/app-instrumentation": "/docs/instrumentation/tracing-sdk",
"#/recording-traces": "/docs/instrumentation/tracing-sdk#recording",
"#/running": "/docs/quickstart/android-tracing",
"#/long-traces": "/docs/concepts/config#long-traces",
"#/detached-mode": "/docs/concepts/detached-mode",
"#/heapprofd": "/docs/data-sources/native-heap-profiler",
"#/java-hprof": "/docs/data-sources/java-heap-profiler",
"#/trace-processor": "/docs/analysis/trace-processor",
"#/analysis": "/docs/analysis/trace-processor#annotations",
"#/metrics": "/docs/analysis/metrics",
"#/traceconv": "/docs/quickstart/traceconv",
"#/clock-sync": "/docs/concepts/clock-sync",
"#/architecture": "/docs/concepts/service-model",
};
const fragment = location.hash.split("?")[0].replace(".md", "");
if (fragment in legacyRedirectMap) {
location.replace(legacyRedirectMap[fragment]);
}
// Pages which have been been removed/renamed/moved and need to be redirected
// to their new home.
const redirectMap = {
// stdlib docs is not a perfect replacement but is good enough until we write
// a proper, Android specific query codelab page.
// TODO(lalitm): switch to that page when it's ready.
"/docs/analysis/common-queries": "/docs/analysis/stdlib-docs",
};
if (location.pathname in redirectMap) {
location.replace(redirectMap[location.pathname]);
}