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