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