1/* 2 * Copyright (c) 2022 Nordic Semiconductor ASA 3 * SPDX-License-Identifier: Apache-2.0 4 */ 5 6const DB_FILE = 'kconfig.json'; 7const RESULTS_PER_PAGE_OPTIONS = [10, 25, 50]; 8let zephyr_gh_base_url; 9let zephyr_version; 10 11/* search state */ 12let db; 13let searchOffset; 14let maxResults = RESULTS_PER_PAGE_OPTIONS[0]; 15 16/* elements */ 17let input; 18let searchTools; 19let summaryText; 20let results; 21let navigation; 22let navigationPagesText; 23let navigationPrev; 24let navigationNext; 25 26/** 27 * Show an error message. 28 * @param {String} message Error message. 29 */ 30function showError(message) { 31 const admonition = document.createElement('div'); 32 admonition.className = 'admonition error'; 33 results.replaceChildren(admonition); 34 35 const admonitionTitle = document.createElement('p'); 36 admonitionTitle.className = 'admonition-title'; 37 admonition.appendChild(admonitionTitle); 38 39 const admonitionTitleText = document.createTextNode('Error'); 40 admonitionTitle.appendChild(admonitionTitleText); 41 42 const admonitionContent = document.createElement('p'); 43 admonition.appendChild(admonitionContent); 44 45 const admonitionContentText = document.createTextNode(message); 46 admonitionContent.appendChild(admonitionContentText); 47} 48 49/** 50 * Show a progress message. 51 * @param {String} message Progress message. 52 */ 53function showProgress(message) { 54 const p = document.createElement('p'); 55 p.className = 'centered'; 56 results.replaceChildren(p); 57 58 const pText = document.createTextNode(message); 59 p.appendChild(pText); 60} 61 62/** 63 * Generate a GitHub link for a given file path in the Zephyr repository. 64 * @param {string} path - The file path in the repository. 65 * @param {number} [line] - Optional line number to link to. 66 * @param {string} [mode=blob] - The mode (blob or edit). Defaults to 'blob'. 67 * @param {string} [revision=main] - The branch, tag, or commit hash. Defaults to 'main'. 68 * @returns {string} - The generated GitHub URL. 69 */ 70function getGithubLink(path, line, mode = "blob", revision = "main") { 71 if (!zephyr_gh_base_url) { 72 return; 73 } 74 75 let url = [ 76 zephyr_gh_base_url, 77 mode, 78 revision, 79 path 80 ].join("/"); 81 82 if (line !== undefined){ 83 url += `#L${line}`; 84 } 85 86 return url; 87} 88 89 90/** 91 * Render a Kconfig literal property. 92 * @param {Element} parent Parent element. 93 * @param {String} title Title. 94 * @param {Element} contentElement Content Element. 95 */ 96function renderKconfigPropLiteralElement(parent, title, contentElement) 97{ 98 const term = document.createElement('dt'); 99 parent.appendChild(term); 100 101 const termText = document.createTextNode(title); 102 term.appendChild(termText); 103 104 const details = document.createElement('dd'); 105 parent.appendChild(details); 106 107 const code = document.createElement('code'); 108 code.className = 'docutils literal'; 109 details.appendChild(code); 110 111 const literal = document.createElement('span'); 112 literal.className = 'pre'; 113 code.appendChild(literal); 114 115 literal.appendChild(contentElement); 116} 117 118/** 119 * Render a Kconfig literal property. 120 * @param {Element} parent Parent element. 121 * @param {String} title Title. 122 * @param {String} content Content. 123 */ 124function renderKconfigPropLiteral(parent, title, content) { 125 const contentElement = document.createTextNode(content); 126 renderKconfigPropLiteralElement(parent, title, contentElement); 127} 128 129/** 130 * Render a Kconfig list property. 131 * @param {Element} parent Parent element. 132 * @param {String} title Title. 133 * @param {list} elements List of elements. 134 * @param {boolean} linkElements Whether to link elements (treat each element 135 * as an unformatted option) 136 */ 137function renderKconfigPropList(parent, title, elements, linkElements) { 138 if (elements.length === 0) { 139 return; 140 } 141 142 const term = document.createElement('dt'); 143 parent.appendChild(term); 144 145 const termText = document.createTextNode(title); 146 term.appendChild(termText); 147 148 const details = document.createElement('dd'); 149 parent.appendChild(details); 150 151 const list = document.createElement('ul'); 152 list.className = 'simple'; 153 details.appendChild(list); 154 155 elements.forEach(element => { 156 const listItem = document.createElement('li'); 157 list.appendChild(listItem); 158 159 if (linkElements) { 160 const link = document.createElement('a'); 161 link.href = '#' + element; 162 listItem.appendChild(link); 163 164 const linkText = document.createTextNode(element); 165 link.appendChild(linkText); 166 } else { 167 /* using HTML since element content is pre-formatted */ 168 listItem.innerHTML = element; 169 } 170 }); 171} 172 173/** 174 * Render a Kconfig list property. 175 * @param {Element} parent Parent element. 176 * @param {list} elements List of elements. 177 * @returns 178 */ 179function renderKconfigDefaults(parent, defaults, alt_defaults) { 180 if (defaults.length === 0 && alt_defaults.length === 0) { 181 return; 182 } 183 184 const term = document.createElement('dt'); 185 parent.appendChild(term); 186 187 const termText = document.createTextNode('Defaults'); 188 term.appendChild(termText); 189 190 const details = document.createElement('dd'); 191 parent.appendChild(details); 192 193 if (defaults.length > 0) { 194 const list = document.createElement('ul'); 195 list.className = 'simple'; 196 details.appendChild(list); 197 198 defaults.forEach(entry => { 199 const listItem = document.createElement('li'); 200 list.appendChild(listItem); 201 202 /* using HTML since default content may be pre-formatted */ 203 listItem.innerHTML = entry; 204 }); 205 } 206 207 if (alt_defaults.length > 0) { 208 const list = document.createElement('ul'); 209 list.className = 'simple'; 210 list.style.display = 'none'; 211 details.appendChild(list); 212 213 alt_defaults.forEach(entry => { 214 const listItem = document.createElement('li'); 215 list.appendChild(listItem); 216 217 /* using HTML since default content may be pre-formatted */ 218 listItem.innerHTML = ` 219 ${entry[0]} 220 <em>at</em> 221 <code class="docutils literal"> 222 <span class"pre">${entry[1]}</span> 223 </code>`; 224 }); 225 226 const show = document.createElement('a'); 227 show.onclick = () => { 228 if (list.style.display === 'none') { 229 list.style.display = 'block'; 230 } else { 231 list.style.display = 'none'; 232 } 233 }; 234 details.appendChild(show); 235 236 const showText = document.createTextNode('Show/Hide other defaults'); 237 show.appendChild(showText); 238 } 239} 240 241/** 242 * Render a Kconfig entry. 243 * @param {Object} entry Kconfig entry. 244 */ 245function renderKconfigEntry(entry) { 246 const container = document.createElement('dl'); 247 container.className = 'kconfig'; 248 249 /* title (name and permalink) */ 250 const title = document.createElement('dt'); 251 title.className = 'sig sig-object'; 252 container.appendChild(title); 253 254 const name = document.createElement('span'); 255 name.className = 'pre'; 256 title.appendChild(name); 257 258 const nameText = document.createTextNode(entry.name); 259 name.appendChild(nameText); 260 261 const permalink = document.createElement('a'); 262 permalink.className = 'headerlink'; 263 permalink.href = '#' + entry.name; 264 title.appendChild(permalink); 265 266 const permalinkText = document.createTextNode('\uf0c1'); 267 permalink.appendChild(permalinkText); 268 269 /* details */ 270 const details = document.createElement('dd'); 271 container.append(details); 272 273 /* prompt and help */ 274 const prompt = document.createElement('p'); 275 details.appendChild(prompt); 276 277 const promptTitle = document.createElement('em'); 278 prompt.appendChild(promptTitle); 279 280 const promptTitleText = document.createTextNode(''); 281 promptTitle.appendChild(promptTitleText); 282 if (entry.prompt) { 283 promptTitleText.nodeValue = entry.prompt; 284 } else { 285 promptTitleText.nodeValue = 'No prompt - not directly user assignable.'; 286 } 287 288 if (entry.help) { 289 const help = document.createElement('p'); 290 details.appendChild(help); 291 292 const helpText = document.createTextNode(entry.help); 293 help.appendChild(helpText); 294 } 295 296 /* symbol properties (defaults, selects, etc.) */ 297 const props = document.createElement('dl'); 298 props.className = 'field-list simple'; 299 details.appendChild(props); 300 301 renderKconfigPropLiteral(props, 'Type', entry.type); 302 if (entry.dependencies) { 303 renderKconfigPropList(props, 'Dependencies', [entry.dependencies]); 304 } 305 renderKconfigDefaults(props, entry.defaults, entry.alt_defaults); 306 renderKconfigPropList(props, 'Selects', entry.selects, false); 307 renderKconfigPropList(props, 'Selected by', entry.selected_by, true); 308 renderKconfigPropList(props, 'Implies', entry.implies, false); 309 renderKconfigPropList(props, 'Implied by', entry.implied_by, true); 310 renderKconfigPropList(props, 'Ranges', entry.ranges, false); 311 renderKconfigPropList(props, 'Choices', entry.choices, false); 312 313 /* symbol location with permalink */ 314 const locationElement = document.createTextNode(`${entry.filename}:${entry.linenr}`); 315 locationElement.class = "pre"; 316 317 let locationPermalink = getGithubLink(entry.filename, entry.linenr, "blob", zephyr_version); 318 if (locationPermalink) { 319 const locationPermalink = document.createElement('a'); 320 locationPermalink.href = locationPermalink; 321 locationPermalink.appendChild(locationElement); 322 renderKconfigPropLiteralElement(props, 'Location', locationPermalink); 323 } else { 324 renderKconfigPropLiteralElement(props, 'Location', locationElement); 325 } 326 327 renderKconfigPropLiteral(props, 'Menu path', entry.menupath); 328 329 return container; 330} 331 332/** Perform a search and display the results. */ 333function doSearch() { 334 /* replace current state (to handle back button) */ 335 history.replaceState({ 336 value: input.value, 337 searchOffset: searchOffset 338 }, '', window.location); 339 340 /* nothing to search for */ 341 if (!input.value) { 342 results.replaceChildren(); 343 navigation.style.visibility = 'hidden'; 344 searchTools.style.visibility = 'hidden'; 345 return; 346 } 347 348 /* perform search */ 349 const regexes = input.value.trim().split(/\s+/).map( 350 element => new RegExp(element.toLowerCase()) 351 ); 352 let count = 0; 353 354 const searchResults = db.filter(entry => { 355 let matches = 0; 356 const name = entry.name.toLowerCase(); 357 const prompt = entry.prompt ? entry.prompt.toLowerCase() : ""; 358 359 regexes.forEach(regex => { 360 if (name.search(regex) >= 0 || prompt.search(regex) >= 0) { 361 matches++; 362 } 363 }); 364 365 if (matches === regexes.length) { 366 count++; 367 if (count > searchOffset && count <= (searchOffset + maxResults)) { 368 return true; 369 } 370 } 371 372 return false; 373 }); 374 375 /* show results count and search tools */ 376 summaryText.nodeValue = `${count} options match your search.`; 377 searchTools.style.visibility = 'visible'; 378 379 /* update navigation */ 380 navigation.style.visibility = 'visible'; 381 navigationPrev.disabled = searchOffset - maxResults < 0; 382 navigationNext.disabled = searchOffset + maxResults > count; 383 384 const currentPage = Math.floor(searchOffset / maxResults) + 1; 385 const totalPages = Math.floor(count / maxResults) + 1; 386 navigationPagesText.nodeValue = `Page ${currentPage} of ${totalPages}`; 387 388 /* render Kconfig entries */ 389 results.replaceChildren(); 390 searchResults.forEach(entry => { 391 results.appendChild(renderKconfigEntry(entry)); 392 }); 393} 394 395/** Do a search from URL hash */ 396function doSearchFromURL() { 397 const rawOption = window.location.hash.substring(1); 398 if (!rawOption) { 399 return; 400 } 401 402 const option = decodeURIComponent(rawOption); 403 if (option.startsWith('!')) { 404 input.value = option.substring(1); 405 } else { 406 input.value = '^' + option + '$'; 407 } 408 409 searchOffset = 0; 410 doSearch(); 411} 412 413function setupKconfigSearch() { 414 /* populate kconfig-search container */ 415 const container = document.getElementById('__kconfig-search'); 416 if (!container) { 417 console.error("Couldn't find Kconfig search container"); 418 return; 419 } 420 421 /* create input field */ 422 const inputContainer = document.createElement('div'); 423 inputContainer.className = 'input-container' 424 container.appendChild(inputContainer) 425 426 input = document.createElement('input'); 427 input.placeholder = 'Type a Kconfig option name (RegEx allowed)'; 428 input.type = 'text'; 429 inputContainer.appendChild(input); 430 431 const copyLinkButton = document.createElement('button'); 432 copyLinkButton.title = "Copy link to results"; 433 copyLinkButton.onclick = () => { 434 if (!window.isSecureContext) { 435 console.error("Cannot copy outside of a secure context"); 436 return; 437 } 438 439 const copyURL = window.location.protocol + '//' + window.location.host + 440 window.location.pathname + '#!' + input.value; 441 442 navigator.clipboard.writeText(encodeURI(copyURL)); 443 } 444 inputContainer.appendChild(copyLinkButton) 445 446 const copyLinkText = document.createTextNode(''); 447 copyLinkButton.appendChild(copyLinkText); 448 449 /* create search tools container */ 450 searchTools = document.createElement('div'); 451 searchTools.className = 'search-tools'; 452 searchTools.style.visibility = 'hidden'; 453 container.appendChild(searchTools); 454 455 /* create search summary */ 456 const searchSummaryContainer = document.createElement('div'); 457 searchTools.appendChild(searchSummaryContainer); 458 459 const searchSummary = document.createElement('p'); 460 searchSummaryContainer.appendChild(searchSummary); 461 462 summaryText = document.createTextNode(''); 463 searchSummary.appendChild(summaryText); 464 465 /* create results per page selector */ 466 const resultsPerPageContainer = document.createElement('div'); 467 resultsPerPageContainer.className = 'results-per-page-container'; 468 searchTools.appendChild(resultsPerPageContainer); 469 470 const resultsPerPageTitle = document.createElement('span'); 471 resultsPerPageTitle.className = 'results-per-page-title'; 472 resultsPerPageContainer.appendChild(resultsPerPageTitle); 473 474 const resultsPerPageTitleText = document.createTextNode('Results per page:'); 475 resultsPerPageTitle.appendChild(resultsPerPageTitleText); 476 477 const resultsPerPageSelect = document.createElement('select'); 478 resultsPerPageSelect.onchange = (event) => { 479 maxResults = parseInt(event.target.value); 480 searchOffset = 0; 481 doSearch(); 482 } 483 resultsPerPageContainer.appendChild(resultsPerPageSelect); 484 485 RESULTS_PER_PAGE_OPTIONS.forEach((value, index) => { 486 const option = document.createElement('option'); 487 option.value = value; 488 option.text = value; 489 option.selected = index === 0; 490 resultsPerPageSelect.appendChild(option); 491 }); 492 493 /* create search results container */ 494 results = document.createElement('div'); 495 container.appendChild(results); 496 497 /* create search navigation */ 498 navigation = document.createElement('div'); 499 navigation.className = 'search-nav'; 500 navigation.style.visibility = 'hidden'; 501 container.appendChild(navigation); 502 503 navigationPrev = document.createElement('button'); 504 navigationPrev.className = 'btn'; 505 navigationPrev.disabled = true; 506 navigationPrev.onclick = () => { 507 searchOffset -= maxResults; 508 doSearch(); 509 window.scroll(0, 0); 510 } 511 navigation.appendChild(navigationPrev); 512 513 const navigationPrevText = document.createTextNode('Previous'); 514 navigationPrev.appendChild(navigationPrevText); 515 516 const navigationPages = document.createElement('p'); 517 navigation.appendChild(navigationPages); 518 519 navigationPagesText = document.createTextNode(''); 520 navigationPages.appendChild(navigationPagesText); 521 522 navigationNext = document.createElement('button'); 523 navigationNext.className = 'btn'; 524 navigationNext.disabled = true; 525 navigationNext.onclick = () => { 526 searchOffset += maxResults; 527 doSearch(); 528 window.scroll(0, 0); 529 } 530 navigation.appendChild(navigationNext); 531 532 const navigationNextText = document.createTextNode('Next'); 533 navigationNext.appendChild(navigationNextText); 534 535 /* load database */ 536 showProgress('Loading database...'); 537 538 fetch(DB_FILE) 539 .then(response => response.json()) 540 .then(json => { 541 db = json["symbols"]; 542 zephyr_gh_base_url = json["gh_base_url"]; 543 zephyr_version = json["zephyr_version"]; 544 545 results.replaceChildren(); 546 547 /* perform initial search */ 548 doSearchFromURL(); 549 550 /* install event listeners */ 551 input.addEventListener('input', () => { 552 searchOffset = 0; 553 doSearch(); 554 }); 555 556 /* install hash change listener (for links) */ 557 window.addEventListener('hashchange', doSearchFromURL); 558 559 /* handle back/forward navigation */ 560 window.addEventListener('popstate', (event) => { 561 if (!event.state) { 562 return; 563 } 564 565 input.value = event.state.value; 566 searchOffset = event.state.searchOffset; 567 doSearch(); 568 }); 569 }) 570 .catch(error => { 571 showError(`Kconfig database could not be loaded (${error})`); 572 }); 573} 574 575setupKconfigSearch(); 576