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