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