// 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; // 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', }; 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); } 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(); // Enable animations only after the load event. This is to prevent glitches // when switching pages. document.documentElement.style.setProperty('--anim-enabled', '1') }); const fragment = location.hash.split('?')[0].replace('.md', ''); if (fragment in legacyRedirectMap) { location.replace(legacyRedirectMap[fragment]); }