1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15'use strict'; 16 17let tocAnchors = []; 18let lastMouseOffY = 0; 19let onloadFired = false; 20const postLoadActions = []; 21let tocEventHandlersInstalled = false; 22let resizeObserver = undefined; 23 24// Handles redirects from the old docs.perfetto.dev. 25const legacyRedirectMap = { 26 '#/contributing': '/docs/contributing/getting-started#community', 27 '#/build-instructions': '/docs/contributing/build-instructions', 28 '#/testing': '/docs/contributing/testing', 29 '#/app-instrumentation': '/docs/instrumentation/tracing-sdk', 30 '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording', 31 '#/running': '/docs/quickstart/android-tracing', 32 '#/long-traces': '/docs/concepts/config#long-traces', 33 '#/detached-mode': '/docs/concepts/detached-mode', 34 '#/heapprofd': '/docs/data-sources/native-heap-profiler', 35 '#/java-hprof': '/docs/data-sources/java-heap-profiler', 36 '#/trace-processor': '/docs/analysis/trace-processor', 37 '#/analysis': '/docs/analysis/trace-processor#annotations', 38 '#/metrics': '/docs/analysis/metrics', 39 '#/traceconv': '/docs/quickstart/traceconv', 40 '#/clock-sync': '/docs/concepts/clock-sync', 41 '#/architecture': '/docs/concepts/service-model', 42}; 43 44function doAfterLoadEvent(action) { 45 if (onloadFired) { 46 return action(); 47 } 48 postLoadActions.push(action); 49} 50 51function setupSandwichMenu() { 52 const header = document.querySelector('.site-header'); 53 const docsNav = document.querySelector('.nav'); 54 const menu = header.querySelector('.menu'); 55 menu.addEventListener('click', (e) => { 56 e.preventDefault(); 57 58 // If we are displaying any /docs, toggle the navbar instead (the TOC). 59 if (docsNav) { 60 // |after_first_click| is to avoid spurious transitions on page load. 61 docsNav.classList.add('after_first_click'); 62 updateNav(); 63 setTimeout(() => docsNav.classList.toggle('expanded'), 0); 64 } else { 65 header.classList.toggle('expanded'); 66 } 67 }); 68} 69 70// (Re-)Generates the Table Of Contents for docs (the right-hand-side one). 71function updateTOC() { 72 const tocContainer = document.querySelector('.docs .toc'); 73 if (!tocContainer) 74 return; 75 const toc = document.createElement('ul'); 76 const anchors = document.querySelectorAll('.doc a.anchor'); 77 tocAnchors = []; 78 for (const anchor of anchors) { 79 const li = document.createElement('li'); 80 const link = document.createElement('a'); 81 link.innerText = anchor.parentElement.innerText; 82 link.href = anchor.href; 83 link.onclick = () => { 84 onScroll(link) 85 }; 86 li.appendChild(link); 87 if (anchor.parentElement.tagName === 'H3') 88 li.style.paddingLeft = '10px'; 89 toc.appendChild(li); 90 doAfterLoadEvent(() => { 91 tocAnchors.push( 92 {top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link}); 93 }); 94 } 95 tocContainer.innerHTML = ''; 96 tocContainer.appendChild(toc); 97 98 // Add event handlers on the first call (can be called more than once to 99 // recompute anchors on resize). 100 if (tocEventHandlersInstalled) 101 return; 102 tocEventHandlersInstalled = true; 103 const doc = document.querySelector('.doc'); 104 const passive = {passive: true}; 105 if (doc) { 106 const offY = doc.offsetTop; 107 doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive); 108 doc.addEventListener('mouseleave', () => { 109 lastMouseOffY = 0; 110 }, passive); 111 } 112 window.addEventListener('scroll', () => onScroll(), passive); 113 resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => { 114 updateNav(); 115 updateTOC(); 116 })); 117 resizeObserver.observe(doc); 118} 119 120// Highlights the current TOC anchor depending on the scroll offset. 121function onMouseMove(offY, e) { 122 lastMouseOffY = e.clientY - offY; 123 onScroll(); 124} 125 126function onScroll(forceHighlight) { 127 const y = document.documentElement.scrollTop + lastMouseOffY; 128 let highEl = undefined; 129 for (const x of tocAnchors) { 130 if (y < x.top) 131 continue; 132 highEl = x.obj; 133 } 134 for (const link of document.querySelectorAll('.docs .toc a')) { 135 if ((!forceHighlight && link === highEl) || (forceHighlight === link)) { 136 link.classList.add('highlighted'); 137 } else { 138 link.classList.remove('highlighted'); 139 } 140 } 141} 142 143// This function needs to be idempotent as it is called more than once (on every 144// resize). 145function updateNav() { 146 const curDoc = document.querySelector('.doc'); 147 let curFileName = ''; 148 if (curDoc) 149 curFileName = curDoc.dataset['mdFile']; 150 151 // First identify all the top-level nav entries (Quickstart, Data Sources, 152 // ...) and make them compressible. 153 const toplevelSections = document.querySelectorAll('.docs .nav > ul > li'); 154 const toplevelLinks = []; 155 for (const sec of toplevelSections) { 156 const childMenu = sec.querySelector('ul'); 157 if (!childMenu) { 158 // Don't make it compressible if it has no children (e.g. the very 159 // first 'Introduction' link). 160 continue; 161 } 162 163 // Don't make it compressible if the entry has an actual link (e.g. the very 164 // first 'Introduction' link), because otherwise it become ambiguous whether 165 // the link should toggle or open the link. 166 const link = sec.querySelector('a'); 167 if (!link || !link.href.endsWith('#')) 168 continue; 169 170 sec.classList.add('compressible'); 171 172 // Remember the compressed status as long as the page is opened, so clicking 173 // through links keeps the sidebar in a consistent visual state. 174 const memoKey = `docs.nav.compressed[${link.innerHTML}]`; 175 176 if (sessionStorage.getItem(memoKey) === '1') { 177 sec.classList.add('compressed'); 178 } 179 doAfterLoadEvent(() => { 180 childMenu.style.maxHeight = `${childMenu.scrollHeight + 40}px`; 181 }); 182 183 toplevelLinks.push(link); 184 link.onclick = (evt) => { 185 evt.preventDefault(); 186 sec.classList.toggle('compressed'); 187 if (sec.classList.contains('compressed')) { 188 sessionStorage.setItem(memoKey, '1'); 189 } else { 190 sessionStorage.removeItem(memoKey); 191 } 192 }; 193 } 194 195 const exps = document.querySelectorAll('.docs .nav ul a'); 196 let found = false; 197 for (const x of exps) { 198 // If the url of the entry matches the url of the page, mark the item as 199 // highlighted and expand all its parents. 200 if (!x.href) 201 continue; 202 const url = new URL(x.href); 203 if (x.href.endsWith('#')) { 204 // This is a non-leaf link to a menu. 205 if (toplevelLinks.indexOf(x) < 0) { 206 x.removeAttribute('href'); 207 } 208 } else if (url.pathname === curFileName && !found) { 209 x.classList.add('selected'); 210 doAfterLoadEvent(() => x.scrollIntoViewIfNeeded()); 211 found = true; // Highlight only the first occurrence. 212 } 213 } 214} 215 216// If the page contains a ```mermaid ``` block, lazily loads the plugin and 217// renders. 218function initMermaid() { 219 const graphs = document.querySelectorAll('.mermaid'); 220 221 // Skip if there are no mermaid graphs to render. 222 if (!graphs.length) 223 return; 224 225 const script = document.createElement('script'); 226 script.type = 'text/javascript'; 227 script.src = '/assets/mermaid.min.js'; 228 const themeCSS = ` 229 .cluster rect { fill: #FCFCFC; stroke: #ddd } 230 .node rect { fill: #DCEDC8; stroke: #8BC34A} 231 .edgeLabel:not(:empty) { 232 border-radius: 6px; 233 font-size: 0.9em; 234 padding: 4px; 235 background: #F5F5F5; 236 border: 1px solid #DDDDDD; 237 color: #666; 238 } 239 `; 240 script.addEventListener('load', () => { 241 mermaid.initialize({ 242 startOnLoad: false, 243 themeCSS: themeCSS, 244 securityLevel: 'loose', // To allow #in-page-links 245 }); 246 for (const graph of graphs) { 247 requestAnimationFrame(() => { 248 mermaid.init(undefined, graph); 249 graph.classList.add('rendered'); 250 }); 251 } 252 }) 253 document.body.appendChild(script); 254} 255 256function setupSearch() { 257 const URL = 258 'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q=' 259 const searchContainer = document.getElementById('search'); 260 const searchBox = document.getElementById('search-box'); 261 const searchRes = document.getElementById('search-res') 262 if (!searchBox || !searchRes) return; 263 264 document.body.addEventListener('keydown', (e) => { 265 if (e.key === '/' && e.target.tagName.toLowerCase() === 'body') { 266 searchBox.setSelectionRange(0, -1); 267 searchBox.focus(); 268 e.preventDefault(); 269 } else if (e.key === 'Escape' && searchContainer.contains(e.target)) { 270 searchBox.blur(); 271 272 // Handle the case of clicking Tab and moving down to results. 273 e.target.blur(); 274 } 275 }); 276 277 let timerId = -1; 278 let lastSearchId = 0; 279 280 const doSearch = async () => { 281 timerId = -1; 282 searchRes.style.width = `${searchBox.offsetWidth}px`; 283 284 // `searchId` handles the case of two subsequent requests racing. This is to 285 // prevent older results, delivered in reverse order, to replace newer ones. 286 const searchId = ++lastSearchId; 287 const f = await fetch(URL + encodeURIComponent(searchBox.value)); 288 const jsonRes = await f.json(); 289 const results = jsonRes['items']; 290 searchRes.innerHTML = ''; 291 if (results === undefined || searchId != lastSearchId) { 292 return; 293 } 294 for (const res of results) { 295 const link = document.createElement('a'); 296 link.href = res.link; 297 const title = document.createElement('div'); 298 title.className = 'sr-title'; 299 title.innerText = res.title.replace(' - Perfetto Tracing Docs', ''); 300 link.appendChild(title); 301 302 const snippet = document.createElement('div'); 303 snippet.className = 'sr-snippet'; 304 snippet.innerText = res.snippet; 305 link.appendChild(snippet); 306 307 const div = document.createElement('div'); 308 div.appendChild(link); 309 searchRes.appendChild(div); 310 } 311 }; 312 313 searchBox.addEventListener('keyup', () => { 314 if (timerId >= 0) return; 315 timerId = setTimeout(doSearch, 200); 316 }); 317} 318 319window.addEventListener('DOMContentLoaded', () => { 320 updateNav(); 321 updateTOC(); 322}); 323 324window.addEventListener('load', () => { 325 setupSandwichMenu(); 326 initMermaid(); 327 328 // Don't smooth-scroll on pages that are too long (e.g. reference pages). 329 if (document.body.scrollHeight < 10000) { 330 document.documentElement.style.scrollBehavior = 'smooth'; 331 } else { 332 document.documentElement.style.scrollBehavior = 'initial'; 333 } 334 335 onloadFired = true; 336 while (postLoadActions.length > 0) { 337 postLoadActions.shift()(); 338 } 339 340 updateTOC(); 341 setupSearch(); 342 343 // Enable animations only after the load event. This is to prevent glitches 344 // when switching pages. 345 document.documentElement.style.setProperty('--anim-enabled', '1') 346}); 347 348const fragment = location.hash.split('?')[0].replace('.md', ''); 349if (fragment in legacyRedirectMap) { 350 location.replace(legacyRedirectMap[fragment]); 351}