doc: extensions: add kconfig search extension
Add a new extension to handle Kconfig documentation. This means that no more CMake hackery is required. However, the way it works differs from the current approach. Instead of creating a single page for each Kconfig option, the extension creates a JSON "database" which is then used on the client side to render Kconfig options on a search page. The reason to go for a single page choice is because Sphinx is significantly slow when handling a lot of pages. Kconfig was responsible for an increase of about ~10K pages. Main features: - Generates a Kconfig JSON database using kconfiglib APIs. - Adds a new Sphinx domain for Kconfig. The domain provides a directive, :kconfig:search:: that can be used to insert a Kconfig search box onto any page. This page is where all Kconfig references inserted using the :kconfig:option: role will point to. The search functionality is implemented on the client side using Javascript. If the URL contains a hash with a Kconfig option (e.g. #CONFIG_SPI) it will load it. Signed-off-by: Gerard Marull-Paretas <gerard.marull@nordicsemi.no>
This commit is contained in:
parent
d2a56c5047
commit
8bdeac62bb
398
doc/_extensions/zephyr/kconfig/__init__.py
Normal file
398
doc/_extensions/zephyr/kconfig/__init__.py
Normal file
|
@ -0,0 +1,398 @@
|
|||
"""
|
||||
Kconfig Extension
|
||||
#################
|
||||
|
||||
Copyright (c) 2022 Nordic Semiconductor ASA
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
This extension adds a new domain (``kconfig``) for the Kconfig language. Unlike
|
||||
many other domains, the Kconfig options are not rendered by Sphinx directly but
|
||||
on the client side using a database built by the extension. A special directive
|
||||
``.. kconfig:search::`` can be inserted on any page to render a search box that
|
||||
allows to browse the database. References to Kconfig options can be created by
|
||||
using the ``:kconfig:option:`` role. Kconfig options behave as regular domain
|
||||
objects, so they can also be referenced by other projects using Intersphinx.
|
||||
|
||||
Options
|
||||
=======
|
||||
|
||||
- kconfig_generate_db: Set to True if you want to generate the Kconfig database.
|
||||
This is only required if you want to use the ``.. kconfig:search::``
|
||||
directive, not if you just need support for Kconfig domain (e.g. when using
|
||||
Intersphinx in another project). Defaults to False.
|
||||
- kconfig_ext_paths: A list of base paths where to search for external modules
|
||||
Kconfig files when they use ``kconfig-ext: True``. The extension will look for
|
||||
${BASE_PATH}/modules/${MODULE_NAME}/Kconfig.
|
||||
"""
|
||||
|
||||
from distutils.command.build import build
|
||||
from itertools import chain
|
||||
import json
|
||||
from operator import mod
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from docutils import nodes
|
||||
from sphinx.addnodes import pending_xref
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.domains import Domain, ObjType
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.errors import ExtensionError
|
||||
from sphinx.roles import XRefRole
|
||||
from sphinx.util import progress_message
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
from sphinx.util.nodes import make_refnode
|
||||
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
RESOURCES_DIR = Path(__file__).parent / "static"
|
||||
ZEPHYR_BASE = Path(__file__).parents[4]
|
||||
|
||||
SCRIPTS = ZEPHYR_BASE / "scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
KCONFIGLIB = SCRIPTS / "kconfig"
|
||||
sys.path.insert(0, str(KCONFIGLIB))
|
||||
|
||||
import zephyr_module
|
||||
import kconfiglib
|
||||
|
||||
|
||||
def kconfig_load(app: Sphinx) -> Tuple[kconfiglib.Kconfig, Dict[str, str]]:
|
||||
"""Load Kconfig"""
|
||||
with TemporaryDirectory() as td:
|
||||
projects = zephyr_module.west_projects()
|
||||
projects = [p.posixpath for p in projects["projects"]] if projects else None
|
||||
modules = zephyr_module.parse_modules(ZEPHYR_BASE, projects)
|
||||
|
||||
# generate Kconfig.modules file
|
||||
kconfig = ""
|
||||
for module in modules:
|
||||
kconfig += zephyr_module.process_kconfig(module.project, module.meta)
|
||||
|
||||
with open(Path(td) / "Kconfig.modules", "w") as f:
|
||||
f.write(kconfig)
|
||||
|
||||
# base environment
|
||||
os.environ["ZEPHYR_BASE"] = str(ZEPHYR_BASE)
|
||||
os.environ["srctree"] = str(ZEPHYR_BASE)
|
||||
os.environ["KCONFIG_DOC_MODE"] = "1"
|
||||
os.environ["KCONFIG_BINARY_DIR"] = td
|
||||
|
||||
# include all archs and boards
|
||||
os.environ["ARCH_DIR"] = "arch"
|
||||
os.environ["ARCH"] = "*"
|
||||
os.environ["BOARD_DIR"] = "boards/*/*"
|
||||
|
||||
# insert external Kconfigs to the environment
|
||||
module_paths = dict()
|
||||
for module in modules:
|
||||
name = module.meta["name"]
|
||||
name_var = module.meta["name-sanitized"].upper()
|
||||
module_paths[name] = module.project
|
||||
|
||||
build_conf = module.meta.get("build")
|
||||
if not build_conf:
|
||||
continue
|
||||
|
||||
if build_conf.get("kconfig"):
|
||||
kconfig = Path(module.project) / build_conf["kconfig"]
|
||||
os.environ[f"ZEPHYR_{name_var}_KCONFIG"] = str(kconfig)
|
||||
elif build_conf.get("kconfig-ext"):
|
||||
for path in app.config.kconfig_ext_paths:
|
||||
kconfig = Path(path) / "modules" / name / "Kconfig"
|
||||
if kconfig.exists():
|
||||
os.environ[f"ZEPHYR_{name_var}_KCONFIG"] = str(kconfig)
|
||||
|
||||
return kconfiglib.Kconfig(ZEPHYR_BASE / "Kconfig"), module_paths
|
||||
|
||||
|
||||
class KconfigSearchNode(nodes.Element):
|
||||
@staticmethod
|
||||
def html():
|
||||
return '<div id="__kconfig-search"></div>'
|
||||
|
||||
|
||||
def kconfig_search_visit_html(self, node: nodes.Node) -> None:
|
||||
self.body.append(node.html())
|
||||
raise nodes.SkipNode
|
||||
|
||||
|
||||
def kconfig_search_visit_latex(self, node: nodes.Node) -> None:
|
||||
self.body.append("Kconfig search is only available on HTML output")
|
||||
raise nodes.SkipNode
|
||||
|
||||
|
||||
class KconfigSearch(SphinxDirective):
|
||||
"""Kconfig search directive"""
|
||||
|
||||
has_content = False
|
||||
|
||||
def run(self):
|
||||
if not self.config.kconfig_generate_db:
|
||||
raise ExtensionError(
|
||||
"Kconfig search directive can not be used without database"
|
||||
)
|
||||
|
||||
if "kconfig_search_inserted" in self.env.temp_data:
|
||||
raise ExtensionError("Kconfig search directive can only be used once")
|
||||
|
||||
self.env.temp_data["kconfig_search_inserted"] = True
|
||||
|
||||
# register all options to the domain at this point, so that they all
|
||||
# resolve to the page where the kconfig:search directive is inserted
|
||||
domain = self.env.get_domain("kconfig")
|
||||
unique = set({option["name"] for option in self.env.kconfig_db})
|
||||
for option in unique:
|
||||
domain.add_option(option)
|
||||
|
||||
return [KconfigSearchNode()]
|
||||
|
||||
|
||||
class _FindKconfigSearchDirectiveVisitor(nodes.NodeVisitor):
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
self._found = False
|
||||
|
||||
def unknown_visit(self, node: nodes.Node) -> None:
|
||||
if self._found:
|
||||
return
|
||||
|
||||
self._found = isinstance(node, KconfigSearchNode)
|
||||
|
||||
@property
|
||||
def found_kconfig_search_directive(self) -> bool:
|
||||
return self._found
|
||||
|
||||
|
||||
class KconfigDomain(Domain):
|
||||
"""Kconfig domain"""
|
||||
|
||||
name = "kconfig"
|
||||
label = "Kconfig"
|
||||
object_types = {"option": ObjType("option", "option")}
|
||||
roles = {"option": XRefRole()}
|
||||
directives = {"search": KconfigSearch}
|
||||
initial_data: Dict[str, Any] = {"options": []}
|
||||
|
||||
def get_objects(self) -> Iterable[Tuple[str, str, str, str, str, int]]:
|
||||
for obj in self.data["options"]:
|
||||
yield obj
|
||||
|
||||
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
|
||||
self.data["options"] += otherdata["options"]
|
||||
|
||||
def resolve_xref(
|
||||
self,
|
||||
env: BuildEnvironment,
|
||||
fromdocname: str,
|
||||
builder: Builder,
|
||||
typ: str,
|
||||
target: str,
|
||||
node: pending_xref,
|
||||
contnode: nodes.Element,
|
||||
) -> Optional[nodes.Element]:
|
||||
match = [
|
||||
(docname, anchor)
|
||||
for name, _, _, docname, anchor, _ in self.get_objects()
|
||||
if name == target
|
||||
]
|
||||
|
||||
if match:
|
||||
todocname, anchor = match[0]
|
||||
|
||||
return make_refnode(
|
||||
builder, fromdocname, todocname, anchor, contnode, anchor
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def add_option(self, option):
|
||||
"""Register a new Kconfig option to the domain."""
|
||||
|
||||
self.data["options"].append(
|
||||
(option, option, "option", self.env.docname, option, -1)
|
||||
)
|
||||
|
||||
|
||||
def sc_fmt(sc):
|
||||
if isinstance(sc, kconfiglib.Symbol):
|
||||
if sc.nodes:
|
||||
return f'<a href="#CONFIG_{sc.name}">CONFIG_{sc.name}</a>'
|
||||
elif isinstance(sc, kconfiglib.Choice):
|
||||
if not sc.name:
|
||||
return "<choice>"
|
||||
return f'<choice <a href="#CONFIG_{sc.name}">CONFIG_{sc.name}</a>>'
|
||||
|
||||
return kconfiglib.standard_sc_expr_str(sc)
|
||||
|
||||
|
||||
def kconfig_build_resources(app: Sphinx) -> None:
|
||||
"""Build the Kconfig database and install HTML resources."""
|
||||
|
||||
if not app.config.kconfig_generate_db:
|
||||
return
|
||||
|
||||
with progress_message("Building Kconfig database..."):
|
||||
kconfig, module_paths = kconfig_load(app)
|
||||
db = list()
|
||||
|
||||
for sc in chain(kconfig.unique_defined_syms, kconfig.unique_choices):
|
||||
# skip nameless symbols
|
||||
if not sc.name:
|
||||
continue
|
||||
|
||||
# store alternative defaults (from defconfig files)
|
||||
alt_defaults = list()
|
||||
for node in sc.nodes:
|
||||
if "defconfig" not in node.filename:
|
||||
continue
|
||||
|
||||
for value, cond in node.orig_defaults:
|
||||
fmt = kconfiglib.expr_str(value, sc_fmt)
|
||||
if cond is not sc.kconfig.y:
|
||||
fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}"
|
||||
alt_defaults.append([fmt, node.filename])
|
||||
|
||||
# only process nodes with prompt or help
|
||||
nodes = [node for node in sc.nodes if node.prompt or node.help]
|
||||
|
||||
inserted_paths = list()
|
||||
for node in nodes:
|
||||
# avoid duplicate symbols by forcing unique paths. this can
|
||||
# happen due to dependencies on 0, a trick used by some modules
|
||||
path = f"{node.filename}:{node.linenr}"
|
||||
if path in inserted_paths:
|
||||
continue
|
||||
inserted_paths.append(path)
|
||||
|
||||
dependencies = None
|
||||
if node.dep is not sc.kconfig.y:
|
||||
dependencies = kconfiglib.expr_str(node.dep, sc_fmt)
|
||||
|
||||
defaults = list()
|
||||
for value, cond in node.orig_defaults:
|
||||
fmt = kconfiglib.expr_str(value, sc_fmt)
|
||||
if cond is not sc.kconfig.y:
|
||||
fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}"
|
||||
defaults.append(fmt)
|
||||
|
||||
selects = list()
|
||||
for value, cond in node.orig_selects:
|
||||
fmt = kconfiglib.expr_str(value, sc_fmt)
|
||||
if cond is not sc.kconfig.y:
|
||||
fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}"
|
||||
selects.append(fmt)
|
||||
|
||||
implies = list()
|
||||
for value, cond in node.orig_implies:
|
||||
fmt = kconfiglib.expr_str(value, sc_fmt)
|
||||
if cond is not sc.kconfig.y:
|
||||
fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}"
|
||||
implies.append(fmt)
|
||||
|
||||
ranges = list()
|
||||
for min, max, cond in node.orig_ranges:
|
||||
fmt = (
|
||||
f"[{kconfiglib.expr_str(min, sc_fmt)}, "
|
||||
f"{kconfiglib.expr_str(max, sc_fmt)}]"
|
||||
)
|
||||
if cond is not sc.kconfig.y:
|
||||
fmt += f" if {kconfiglib.expr_str(cond, sc_fmt)}"
|
||||
ranges.append(fmt)
|
||||
|
||||
choices = list()
|
||||
if isinstance(sc, kconfiglib.Choice):
|
||||
for sym in sc.syms:
|
||||
choices.append(kconfiglib.expr_str(sym, sc_fmt))
|
||||
|
||||
filename = node.filename
|
||||
for name, path in module_paths.items():
|
||||
if node.filename.startswith(path):
|
||||
filename = node.filename.replace(path, f"<module:{name}>")
|
||||
break
|
||||
|
||||
db.append(
|
||||
{
|
||||
"name": f"CONFIG_{sc.name}",
|
||||
"prompt": node.prompt[0] if node.prompt else None,
|
||||
"type": kconfiglib.TYPE_TO_STR[sc.type],
|
||||
"help": node.help,
|
||||
"dependencies": dependencies,
|
||||
"defaults": defaults,
|
||||
"alt_defaults": alt_defaults,
|
||||
"selects": selects,
|
||||
"implies": implies,
|
||||
"ranges": ranges,
|
||||
"choices": choices,
|
||||
"filename": filename,
|
||||
"linenr": node.linenr,
|
||||
}
|
||||
)
|
||||
|
||||
app.env.kconfig_db = db # type: ignore
|
||||
|
||||
outdir = Path(app.outdir) / "kconfig"
|
||||
outdir.mkdir(exist_ok=True)
|
||||
|
||||
kconfig_db_file = outdir / "kconfig.json"
|
||||
|
||||
with open(kconfig_db_file, "w") as f:
|
||||
json.dump(db, f)
|
||||
|
||||
app.config.html_extra_path.append(kconfig_db_file.as_posix())
|
||||
app.config.html_static_path.append(RESOURCES_DIR.as_posix())
|
||||
|
||||
|
||||
def kconfig_install(
|
||||
app: Sphinx,
|
||||
pagename: str,
|
||||
templatename: str,
|
||||
context: Dict,
|
||||
doctree: Optional[nodes.Node],
|
||||
) -> None:
|
||||
"""Install the Kconfig library files on pages that require it."""
|
||||
if (
|
||||
not app.config.kconfig_generate_db
|
||||
or app.builder.format != "html"
|
||||
or not doctree
|
||||
):
|
||||
return
|
||||
|
||||
visitor = _FindKconfigSearchDirectiveVisitor(doctree)
|
||||
doctree.walk(visitor)
|
||||
if visitor.found_kconfig_search_directive:
|
||||
app.add_css_file("kconfig.css")
|
||||
app.add_js_file("kconfig.mjs", type="module")
|
||||
|
||||
|
||||
def setup(app: Sphinx):
|
||||
app.add_config_value("kconfig_generate_db", False, "env")
|
||||
app.add_config_value("kconfig_ext_paths", [], "env")
|
||||
|
||||
app.add_node(
|
||||
KconfigSearchNode,
|
||||
html=(kconfig_search_visit_html, None),
|
||||
latex=(kconfig_search_visit_latex, None),
|
||||
)
|
||||
|
||||
app.add_domain(KconfigDomain)
|
||||
|
||||
app.connect("builder-inited", kconfig_build_resources)
|
||||
app.connect("html-page-context", kconfig_install)
|
||||
|
||||
return {
|
||||
"version": __version__,
|
||||
"parallel_read_safe": True,
|
||||
"parallel_write_safe": True,
|
||||
}
|
37
doc/_extensions/zephyr/kconfig/static/kconfig.css
Normal file
37
doc/_extensions/zephyr/kconfig/static/kconfig.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Nordic Semiconductor ASA
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* Kconfig search */
|
||||
|
||||
#__kconfig-search input {
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(149, 157, 165, 0.2);
|
||||
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px !important;
|
||||
font-size: 18px;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#__kconfig-search .search-summary {
|
||||
margin: 0.25rem 0.1rem 1.5rem;
|
||||
}
|
||||
|
||||
#__kconfig-search .search-nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#__kconfig-search .search-nav > p {
|
||||
padding: 0 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Kconfig entries */
|
||||
|
||||
.kconfig ul {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
428
doc/_extensions/zephyr/kconfig/static/kconfig.mjs
Normal file
428
doc/_extensions/zephyr/kconfig/static/kconfig.mjs
Normal file
|
@ -0,0 +1,428 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Nordic Semiconductor ASA
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const DB_FILE = 'kconfig.json';
|
||||
const MAX_RESULTS = 10;
|
||||
|
||||
/* search state */
|
||||
let db;
|
||||
let searchOffset;
|
||||
|
||||
/* elements */
|
||||
let input;
|
||||
let summaryText;
|
||||
let results;
|
||||
let navigation;
|
||||
let navigationPagesText;
|
||||
let navigationPrev;
|
||||
let navigationNext;
|
||||
|
||||
/**
|
||||
* Show an error message.
|
||||
* @param {String} message Error message.
|
||||
*/
|
||||
function showError(message) {
|
||||
const admonition = document.createElement('div');
|
||||
admonition.className = 'admonition error';
|
||||
results.replaceChildren(admonition);
|
||||
|
||||
const admonitionTitle = document.createElement('p');
|
||||
admonitionTitle.className = 'admonition-title';
|
||||
admonition.appendChild(admonitionTitle);
|
||||
|
||||
const admonitionTitleText = document.createTextNode('Error');
|
||||
admonitionTitle.appendChild(admonitionTitleText);
|
||||
|
||||
const admonitionContent = document.createElement('p');
|
||||
admonition.appendChild(admonitionContent);
|
||||
|
||||
const admonitionContentText = document.createTextNode(message);
|
||||
admonitionContent.appendChild(admonitionContentText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a progress message.
|
||||
* @param {String} message Progress message.
|
||||
*/
|
||||
function showProgress(message) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'centered';
|
||||
results.replaceChildren(p);
|
||||
|
||||
const pText = document.createTextNode(message);
|
||||
p.appendChild(pText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Kconfig literal property.
|
||||
* @param {Element} parent Parent element.
|
||||
* @param {String} title Title.
|
||||
* @param {String} content Content.
|
||||
*/
|
||||
function renderKconfigPropLiteral(parent, title, content) {
|
||||
const term = document.createElement('dt');
|
||||
parent.appendChild(term);
|
||||
|
||||
const termText = document.createTextNode(title);
|
||||
term.appendChild(termText);
|
||||
|
||||
const details = document.createElement('dd');
|
||||
parent.appendChild(details);
|
||||
|
||||
const code = document.createElement('code');
|
||||
code.className = 'docutils literal';
|
||||
details.appendChild(code);
|
||||
|
||||
const literal = document.createElement('span');
|
||||
literal.className = 'pre';
|
||||
code.appendChild(literal);
|
||||
|
||||
const literalText = document.createTextNode(content);
|
||||
literal.appendChild(literalText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Kconfig list property.
|
||||
* @param {Element} parent Parent element.
|
||||
* @param {String} title Title.
|
||||
* @param {list} elements List of elements.
|
||||
* @returns
|
||||
*/
|
||||
function renderKconfigPropList(parent, title, elements) {
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const term = document.createElement('dt');
|
||||
parent.appendChild(term);
|
||||
|
||||
const termText = document.createTextNode(title);
|
||||
term.appendChild(termText);
|
||||
|
||||
const details = document.createElement('dd');
|
||||
parent.appendChild(details);
|
||||
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'simple';
|
||||
details.appendChild(list);
|
||||
|
||||
elements.forEach(element => {
|
||||
const listItem = document.createElement('li');
|
||||
list.appendChild(listItem);
|
||||
|
||||
/* using HTML since element content may be pre-formatted */
|
||||
listItem.innerHTML = element;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Kconfig list property.
|
||||
* @param {Element} parent Parent element.
|
||||
* @param {list} elements List of elements.
|
||||
* @returns
|
||||
*/
|
||||
function renderKconfigDefaults(parent, defaults, alt_defaults) {
|
||||
if (defaults.length === 0 && alt_defaults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const term = document.createElement('dt');
|
||||
parent.appendChild(term);
|
||||
|
||||
const termText = document.createTextNode('Defaults');
|
||||
term.appendChild(termText);
|
||||
|
||||
const details = document.createElement('dd');
|
||||
parent.appendChild(details);
|
||||
|
||||
if (defaults.length > 0) {
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'simple';
|
||||
details.appendChild(list);
|
||||
|
||||
defaults.forEach(entry => {
|
||||
const listItem = document.createElement('li');
|
||||
list.appendChild(listItem);
|
||||
|
||||
/* using HTML since default content may be pre-formatted */
|
||||
listItem.innerHTML = entry;
|
||||
});
|
||||
}
|
||||
|
||||
if (alt_defaults.length > 0) {
|
||||
const list = document.createElement('ul');
|
||||
list.className = 'simple';
|
||||
list.style.display = 'none';
|
||||
details.appendChild(list);
|
||||
|
||||
alt_defaults.forEach(entry => {
|
||||
const listItem = document.createElement('li');
|
||||
list.appendChild(listItem);
|
||||
|
||||
/* using HTML since default content may be pre-formatted */
|
||||
listItem.innerHTML = `
|
||||
${entry[0]}
|
||||
<em>at</em>
|
||||
<code class="docutils literal">
|
||||
<span class"pre">${entry[1]}</span>
|
||||
</code>`;
|
||||
});
|
||||
|
||||
const show = document.createElement('a');
|
||||
show.onclick = () => {
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
}
|
||||
};
|
||||
details.appendChild(show);
|
||||
|
||||
const showText = document.createTextNode('Show/Hide other defaults');
|
||||
show.appendChild(showText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Kconfig entry.
|
||||
* @param {Object} entry Kconfig entry.
|
||||
*/
|
||||
function renderKconfigEntry(entry) {
|
||||
const container = document.createElement('dl');
|
||||
container.className = 'kconfig';
|
||||
|
||||
/* title (name and permalink) */
|
||||
const title = document.createElement('dt');
|
||||
title.className = 'sig sig-object';
|
||||
container.appendChild(title);
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'pre';
|
||||
title.appendChild(name);
|
||||
|
||||
const nameText = document.createTextNode(entry.name);
|
||||
name.appendChild(nameText);
|
||||
|
||||
const permalink = document.createElement('a');
|
||||
permalink.className = 'headerlink';
|
||||
permalink.href = '#' + entry.name;
|
||||
title.appendChild(permalink);
|
||||
|
||||
const permalinkText = document.createTextNode('\uf0c1');
|
||||
permalink.appendChild(permalinkText);
|
||||
|
||||
/* details */
|
||||
const details = document.createElement('dd');
|
||||
container.append(details);
|
||||
|
||||
/* prompt and help */
|
||||
const prompt = document.createElement('p');
|
||||
details.appendChild(prompt);
|
||||
|
||||
const promptTitle = document.createElement('em');
|
||||
prompt.appendChild(promptTitle);
|
||||
|
||||
const promptTitleText = document.createTextNode('');
|
||||
promptTitle.appendChild(promptTitleText);
|
||||
if (entry.prompt) {
|
||||
promptTitleText.nodeValue = entry.prompt;
|
||||
} else {
|
||||
promptTitleText.nodeValue = 'No prompt - not directly user assignable.';
|
||||
}
|
||||
|
||||
if (entry.help) {
|
||||
const help = document.createElement('p');
|
||||
details.appendChild(help);
|
||||
|
||||
const helpText = document.createTextNode(entry.help);
|
||||
help.appendChild(helpText);
|
||||
}
|
||||
|
||||
/* symbol properties (defaults, selects, etc.) */
|
||||
const props = document.createElement('dl');
|
||||
props.className = 'field-list simple';
|
||||
details.appendChild(props);
|
||||
|
||||
renderKconfigPropLiteral(props, 'Type', entry.type);
|
||||
if (entry.dependencies) {
|
||||
renderKconfigPropList(props, 'Dependencies', [entry.dependencies]);
|
||||
}
|
||||
renderKconfigDefaults(props, entry.defaults, entry.alt_defaults);
|
||||
renderKconfigPropList(props, 'Selects', entry.selects);
|
||||
renderKconfigPropList(props, 'Implies', entry.implies);
|
||||
renderKconfigPropList(props, 'Ranges', entry.ranges);
|
||||
renderKconfigPropList(props, 'Choices', entry.choices);
|
||||
renderKconfigPropLiteral(props, 'Location', `${entry.filename}:${entry.linenr}`);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/** Perform a search and display the results. */
|
||||
function doSearch() {
|
||||
/* replace current state (to handle back button) */
|
||||
history.replaceState({
|
||||
value: input.value,
|
||||
searchOffset: searchOffset
|
||||
}, '', window.location);
|
||||
|
||||
/* nothing to search for */
|
||||
if (!input.value) {
|
||||
summaryText.nodeValue = '';
|
||||
results.replaceChildren();
|
||||
navigation.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
/* perform search */
|
||||
let pattern = new RegExp(input.value, 'i');
|
||||
let count = 0;
|
||||
|
||||
const searchResults = db.filter(entry => {
|
||||
if (entry.name.match(pattern)) {
|
||||
count++;
|
||||
if (count > searchOffset && count <= (searchOffset + MAX_RESULTS)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
/* show results count */
|
||||
summaryText.nodeValue = `${count} options match your search.`;
|
||||
|
||||
/* update navigation */
|
||||
navigation.style.visibility = 'visible';
|
||||
navigationPrev.disabled = searchOffset - MAX_RESULTS < 0;
|
||||
navigationNext.disabled = searchOffset + MAX_RESULTS > count;
|
||||
|
||||
const currentPage = Math.floor(searchOffset / MAX_RESULTS) + 1;
|
||||
const totalPages = Math.floor(count / MAX_RESULTS) + 1;
|
||||
navigationPagesText.nodeValue = `Page ${currentPage} of ${totalPages}`;
|
||||
|
||||
/* render Kconfig entries */
|
||||
results.replaceChildren();
|
||||
searchResults.forEach(entry => {
|
||||
results.appendChild(renderKconfigEntry(entry));
|
||||
});
|
||||
}
|
||||
|
||||
/** Do a search from URL hash */
|
||||
function doSearchFromURL() {
|
||||
const rawOption = window.location.hash.substring(1);
|
||||
if (!rawOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const option = rawOption.replace(/[^A-Za-z0-9_]+/g, '');
|
||||
input.value = '^' + option + '$';
|
||||
|
||||
searchOffset = 0;
|
||||
doSearch();
|
||||
}
|
||||
|
||||
function setupKconfigSearch() {
|
||||
/* populate kconfig-search container */
|
||||
const container = document.getElementById('__kconfig-search');
|
||||
if (!container) {
|
||||
console.error("Couldn't find Kconfig search container");
|
||||
return;
|
||||
}
|
||||
|
||||
/* create input field */
|
||||
input = document.createElement('input');
|
||||
input.placeholder = 'Type a Kconfig option name (RegEx allowed)';
|
||||
input.type = 'text';
|
||||
container.appendChild(input);
|
||||
|
||||
/* create search summary */
|
||||
const searchSummary = document.createElement('p');
|
||||
searchSummary.className = 'search-summary';
|
||||
container.appendChild(searchSummary);
|
||||
|
||||
summaryText = document.createTextNode('');
|
||||
searchSummary.appendChild(summaryText);
|
||||
|
||||
/* create search results container */
|
||||
results = document.createElement('div');
|
||||
container.appendChild(results);
|
||||
|
||||
/* create search navigation */
|
||||
navigation = document.createElement('div');
|
||||
navigation.className = 'search-nav';
|
||||
navigation.style.visibility = 'hidden';
|
||||
container.appendChild(navigation);
|
||||
|
||||
navigationPrev = document.createElement('button');
|
||||
navigationPrev.className = 'btn';
|
||||
navigationPrev.disabled = true;
|
||||
navigationPrev.onclick = () => {
|
||||
searchOffset -= MAX_RESULTS;
|
||||
doSearch();
|
||||
window.scroll(0, 0);
|
||||
}
|
||||
navigation.appendChild(navigationPrev);
|
||||
|
||||
const navigationPrevText = document.createTextNode('Previous');
|
||||
navigationPrev.appendChild(navigationPrevText);
|
||||
|
||||
const navigationPages = document.createElement('p');
|
||||
navigation.appendChild(navigationPages);
|
||||
|
||||
navigationPagesText = document.createTextNode('');
|
||||
navigationPages.appendChild(navigationPagesText);
|
||||
|
||||
navigationNext = document.createElement('button');
|
||||
navigationNext.className = 'btn';
|
||||
navigationNext.disabled = true;
|
||||
navigationNext.onclick = () => {
|
||||
searchOffset += MAX_RESULTS;
|
||||
doSearch();
|
||||
window.scroll(0, 0);
|
||||
}
|
||||
navigation.appendChild(navigationNext);
|
||||
|
||||
const navigationNextText = document.createTextNode('Next');
|
||||
navigationNext.appendChild(navigationNextText);
|
||||
|
||||
/* load database */
|
||||
showProgress('Loading database...');
|
||||
|
||||
fetch(DB_FILE)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
db = json;
|
||||
|
||||
results.replaceChildren();
|
||||
|
||||
/* perform initial search */
|
||||
doSearchFromURL();
|
||||
|
||||
/* install event listeners */
|
||||
input.addEventListener('keyup', () => {
|
||||
searchOffset = 0;
|
||||
doSearch();
|
||||
});
|
||||
|
||||
/* install hash change listener (for links) */
|
||||
window.addEventListener('hashchange', doSearchFromURL);
|
||||
|
||||
/* handle back/forward navigation */
|
||||
window.addEventListener('popstate', (event) => {
|
||||
if (!event.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = event.state.value;
|
||||
searchOffset = event.state.searchOffset;
|
||||
doSearch();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
showError(`Kconfig database could not be loaded (${error})`);
|
||||
});
|
||||
}
|
||||
|
||||
setupKconfigSearch();
|
Loading…
Reference in a new issue