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