diff --git a/docs/_extensions/page_filter.py b/docs/_extensions/page_filter.py new file mode 100644 index 00000000..56bc5a86 --- /dev/null +++ b/docs/_extensions/page_filter.py @@ -0,0 +1,252 @@ +""" +Copyright (c) 2023 Nordic Semiconductor ASA + +SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +This extension provides a ``FilterDropdown`` node that generates a dropdown menu +from a list of options, and invokes a javascript filter module that hides all +hideable html elements that does not share a classname with the selected option. + +All hideable html elements must include the classname "hideable". + +Multiple dropdown nodes can be present one the same page. Only elements with html +classes that include all selected dropdown options will be shown. All dropdown +nodes comes with a "show all" option selected by default, unless set by the +:default: option. + +This extension also provides two directives that use the ``FilterDropdown`` node, +``page-filter`` and ``version-filter``. + +The ``page-filter`` directive requires a name and a list of options as its body. +The name is used both as an identifier and to display the default "All x" option. +Every option in the option list should contain the option value contained in one +word, followed by the displayed text for that option. If the option value is +prefixed by "!", it will hide the option values instead of show them. An optional +:default: option can be used to change the default selected value from "all". + +The directive can also generate a visible HTML div element containing a list of +tags. To do this, both the :tags: and :container: options must be present. +The :tags: option should contain a list of tuples (c, d) where c is the classname +the tag will be generated from, and d is the displayed string in the generated tag. +The :container: option should contain an HTML tag "path", where the top level +element is searched for the specified classnames, and the bottom level element is +the parent element for the taglist div. For example, :container: section/a/span +will search every
element for the classnames given in :tags:, create a +tag for each and place them in a div inserted into the element, if such +an element exists within an element within the
element in question. + +Example of use: +.. page-filter:: + :name: components + + ble_controller BLE Controller + nrfcloud nRFCloud + tf-m TF-M + ble_mesh BLE MESH + +The ``version-filter`` directive provides a pre-populated dropdown-list of all +NRF versions to date. Relevant html elements should include all applicable +versions in its classname on a "vX-X-X" format. +The :name: option is defaulted to "versions". +The :tags: option is prepopulated with the tuple ("versions", ""). This +will generate clickable version tags in addition to any other tags specified, +given that the :container: option is present. + +Example of use: +.. version-filter:: + :default: v2-4-0 + :tags: [("wontfix", "Won't fix")] + :container: dl/dt +""" + +from docutils import nodes +from docutils.parsers.rst import directives +from sphinx.util.docutils import SphinxDirective +from sphinx.application import Sphinx +from sphinx.util import logging +from typing import Dict, List, Optional, Tuple +from pathlib import Path +import json +import re + +__version__ = "0.1.0" + +logger = logging.getLogger(__name__) + +RESOURCES_DIR = Path(__file__).parent / "static" +"""Static resources""" + +VERSIONS_FILE = Path(__file__).parents[1] / "versions.json" +"""Contains all versions to date""" + + +class PageFilter(SphinxDirective): + + has_content = True + option_spec = { + "name": directives.unchanged, + "default": directives.unchanged, + "tags": eval, + "container": directives.unchanged, + } + + def run(self): + name = self.options.get("name", "") + split_first = lambda s: s.split(maxsplit=1) + content = list(map(split_first, self.content)) + default = self.options.get("default", "all") + container = self.options.get("container", None) + tags = self.options.get("tags", []) + tags = {classname: displayname for classname, displayname in tags} + return [FilterDropdown(name, content, default, container, tags)] + + +class VersionFilter(PageFilter): + + has_content = False + + def run(self): + name = self.options.get("name", "versions") + default = self.options.get("default", "all") + container_element = self.options.get("container", None) + tags = self.options.get("tags", []) + tags = {classname: displayname for classname, displayname in tags} + tags["versions"] = "" + create_tuple = lambda v: (v, v.replace("-", ".")) + versions = list(map(create_tuple, reversed(self.env.nrf_versions))) + return [FilterDropdown(name, versions, default, container_element, tags)] + + +class FilterDropdown(nodes.Element): + """Generate a dropdown menu for filter selection. + + Args: + name: Unique identifier, also used in the "all" option. + options: List of tuples where the first element is the html value + and the second element is the displayed option text. + default_value: Value selected by default. + container_element: html tag to generate filter tags in (default None). + filter_tags: Tuples of (classname, displayname), where classname is the + class to create a tag from, and displayname is the content + of the tag. + """ + + def __init__( + self, + name: str, + options: List[Tuple[str, str]], + default_value: str = "all", + container_element: str = None, + filter_tags: Tuple[str, str] = None, + ) -> None: + super().__init__() + self.name = name + self.options = options + self.default_value = default_value + self.container_element = container_element + self.filter_tags = filter_tags + + def html(self): + self.options.insert(0, ("all", f"All {self.name}")) + opt_list = [] + for val, text in self.options: + if val == self.default_value: + opt_list.append(f'') + else: + opt_list.append(f'') + + html_str = f'\n" + return html_str + + +def filter_dropdown_visit_html(self, node: nodes.Node) -> None: + self.body.append(node.html()) + raise nodes.SkipNode + + +class _FindFilterDropdownVisitor(nodes.NodeVisitor): + def __init__(self, document): + super().__init__(document) + self._found_dropdowns = [] + + def unknown_visit(self, node: nodes.Node) -> None: + if isinstance(node, FilterDropdown): + self._found_dropdowns.append(node) + + @property + def found_filter_dropdown(self) -> List[nodes.Node]: + return self._found_dropdowns + + +def page_filter_install( + app: Sphinx, + pagename: str, + templatename: str, + context: Dict, + doctree: Optional[nodes.Node], +) -> None: + """Install the javascript filter function.""" + + if app.builder.format != "html" or not doctree: + return + + visitor = _FindFilterDropdownVisitor(doctree) + doctree.walk(visitor) + if visitor.found_filter_dropdown: + app.add_css_file("page_filter.css") + app.add_js_file("page_filter.mjs", type="module") + filename = app.builder.script_files[-1] + + page_depth = len(Path(pagename).parents) - 1 + body = f"import setupFiltering from './{page_depth * '../'}{filename}'; " + for dropdown in visitor.found_filter_dropdown: + body += f"setupFiltering('{dropdown.name}'" + if dropdown.container_element and dropdown.filter_tags: + body += f", '{dropdown.container_element}', {dropdown.filter_tags}" + body += "); " + + app.add_js_file(filename=None, body=body, type="module") + + +def add_filter_resources(app: Sphinx): + app.config.html_static_path.append(RESOURCES_DIR.as_posix()) + read_versions(app) + + +def read_versions(app: Sphinx) -> None: + """Get all NRF versions to date""" + + if hasattr(app.env, "nrf_versions") and app.env.nrf_versions: + return + + try: + with open(VERSIONS_FILE) as version_file: + nrf_versions = json.loads(version_file.read()) + nrf_versions = list( + filter(lambda v: re.match(r"\d\.\d\.\d$", v), nrf_versions) + ) + # Versions classes are on the format "vX-X-X" + app.env.nrf_versions = [ + f"v{version.replace('.', '-')}" for version in reversed(nrf_versions) + ] + except FileNotFoundError: + logger.error("Could not load version file") + app.env.nrf_versions = [] + + +def setup(app: Sphinx): + app.add_directive("page-filter", PageFilter) + app.add_directive("version-filter", VersionFilter) + + app.connect("builder-inited", add_filter_resources) + app.connect("html-page-context", page_filter_install) + + app.add_node(FilterDropdown, html=(filter_dropdown_visit_html, None)) + + return { + "version": __version__, + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_extensions/static/page_filter.css b/docs/_extensions/static/page_filter.css new file mode 100644 index 00000000..52da5b80 --- /dev/null +++ b/docs/_extensions/static/page_filter.css @@ -0,0 +1,47 @@ +.versiontag { + border: 1px solid #e97c25; + padding: 2px; + font-size: smaller; + color: #e97c25; + margin: 0px 5px; +} + +.filtertag { + border: 1px solid red; + padding: 2px; + font-size: smaller; + color: red; + margin: 0px 5px; +} + +.filtertag-container { + overflow-wrap: break-word; + word-break: break-all; + } + +.dropdown-select { + display: inline-block; + font-weight: 700; + color: var(--docset-color); + box-shadow: 0 1px 0 1px rgba(0,0,0,.04); + border: 2px solid #999; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + background-color: #fff; + background-image: url('./images/dropdown.svg'); + background-repeat: no-repeat, repeat; + background-position: right .7em top 50%; + background-size: .65em auto; + width: 160px; + margin-bottom: 30px; + margin-left: 10px; +} + +.dropdown-select::-ms-expand { + display: none; +} + +.dropdown-select option { + font-weight: normal; +} diff --git a/docs/_extensions/static/page_filter.mjs b/docs/_extensions/static/page_filter.mjs new file mode 100644 index 00000000..5e4f88b7 --- /dev/null +++ b/docs/_extensions/static/page_filter.mjs @@ -0,0 +1,171 @@ +const all_dropdowns = [] + +/** + * Show all options or hide all except one + * @param {String} option The option to display + * @param {Element} dropdown The related dropdown + */ +function displayOption(option, dropdown) { + + dropdown.value = option; + + document.querySelectorAll(".hideable").forEach(e => e.hidden = false); + var filters = all_dropdowns + .map(dp => dp.options[dp.selectedIndex].value) + .filter(val => val !== "all"); + + if (filters.length == 0) return; + + var negativeFilters = filters + .filter(val => val.charAt(0) === "!") + .map(val => val.substring(1)); + filters = filters.filter(val => val.charAt(0) !== "!") + + var selector = ""; + if (negativeFilters.length > 0) { + selector += ".hideable." + negativeFilters.join(",.hideable."); + } + if (negativeFilters.length > 0 && filters.length > 0) { + selector += ","; + } + if (filters.length > 0) { + selector += filters.map(f => `.hideable:not(.${f})`).join(",") + } + + document.querySelectorAll(selector).forEach(e => e.hidden = true); +}; + +/** + * Add filter tags to a selected DOM element. + * + * Display classes of the format vX-X-X as clickable tag vX.X.X + * Also ensure that all relevant elements of the chosen type are hideable. + * @param {Element} dropdown The related dropdown + * @param {String} elementType DOM tag to insert version tags into + * @param {Object} filterTags Mapping of classnames to displaynames of filter tags. + */ +function createFilterTags(dropdown, elementType, filterTags) { + var classFilterRE = Object.keys(filterTags); + var index = classFilterRE.indexOf("versions"); + if (index !== -1) { + classFilterRE[index] = "v\\d+-\\d+-\\d+"; + } + var parentElements = elementType.split("/"); + document.querySelectorAll(parentElements.shift()).forEach((element) => { + if (!element.getAttribute("class")) return; + var filterClasses = element + .getAttribute("class") + .split(" ") + .filter(name => RegExp(classFilterRE.join("|")).test(name)); + if (filterClasses.length == 0) return; + + if (!element.classList.contains("hideable")) { + element.classList.add("hideable"); + } + + if (!element.classList.contains("simple")) { + element.classList.add("simple"); + } + + var containerParent = element; + for (var containerElement of parentElements) { + var containerChild = containerParent.querySelector(containerElement); + if (containerChild == null) return; + containerParent = containerChild; + } + + var tagDiv = document.createElement("div"); + tagDiv.classList.add("filtertag-container"); + containerParent.append(tagDiv); + + filterClasses.forEach((className) => { + + var URL = window.location.href.split('?')[0]; + + var aTag = document.createElement("a"); + var spanTag = document.createElement("span"); + + var filterName; + if (className in filterTags) { + spanTag.setAttribute("filter", className); + spanTag.classList.add("filtertag"); + filterName = filterTags[className]; + } + else if (RegExp('v\\d+-\\d+-\\d+').test(className)) { + aTag.setAttribute("href", URL + "?v=" + className); + spanTag.setAttribute("version", className); + spanTag.classList.add("versiontag"); + filterName = className.replace(/v(\d+)-(\d+)-(\d+)/i, 'v$1.$2.$3'); + /** When clicking a version tag, filter by the corresponding version **/ + spanTag.addEventListener("click", () => displayOption(className, dropdown)); + } + var textNode = document.createTextNode(filterName); + + spanTag.appendChild(textNode); + aTag.appendChild(spanTag); + tagDiv.appendChild(aTag); + }); + }); +} + +/** + * Function that retrieves parameter from the URL + * @param {String} param The requested URL parameter + */ +function getUrlParameter(param) { + var urlVariables = window.location.search.substring(1).split('&'); + + for (var i in urlVariables) { + var parameterName = urlVariables[i].split('='); + + if (parameterName[0] === param) { + return parameterName[1] === undefined ? true : decodeURIComponent(parameterName[1]); + } + } +}; + +var ready = (callback) => { + if (document.readyState != "loading") callback(); + else document.addEventListener("DOMContentLoaded", callback); +} + +/** + * Set up appropriate event listener for dropdown with attribute name. + * If filterTagContainer is not None, filter tags are created as a + * child paragraph to all elements with a version class within the + * specified container element. + * + * @param {String} name name attribute identifying the dropdown. + * @param {String} filterTagContainer container element for version tags. + */ +function setupFiltering(name, filterTagContainer=undefined, filterTags={}) { + + ready(() => { + + const dropdown = document.querySelector(`[name='${name}']`); + all_dropdowns.push(dropdown); + + if (filterTagContainer) { + createFilterTags(dropdown, filterTagContainer, filterTags); + } + + /** When selecting a version from the dropdown, filter **/ + dropdown.addEventListener("change", () => { + var value = dropdown.options[dropdown.selectedIndex].value; + displayOption(value, dropdown); + }); + + /** Retrieve the 'v' parameter and switch to that version, if applicable. + Otherwise, switch to the version that is selected in the dropdown. **/ + var v = getUrlParameter('v'); + if ("versions" in filterTags && (RegExp('v\\d+-\\d+-\\d+').test(v))) { + displayOption(v, dropdown); + } + else { + var value = dropdown.options[dropdown.selectedIndex].value; + displayOption(value, dropdown); + }; + }); +} + +export default setupFiltering; diff --git a/docs/conf.py b/docs/conf.py index d6209cf0..d2db8515 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,6 +28,7 @@ 'sphinxcontrib.mscgen', 'sphinx_tabs.tabs', 'sphinx_togglebutton', + 'page_filter', ] templates_path = ['_templates'] diff --git a/docs/index.rst b/docs/index.rst index b89c1f4f..969b760b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,3 +31,4 @@ In combination with the |NCS|, the |addon| allows for development of low-power c lib/index tools release-notes + known_issues diff --git a/docs/known_issues.rst b/docs/known_issues.rst new file mode 100644 index 00000000..3a38a235 --- /dev/null +++ b/docs/known_issues.rst @@ -0,0 +1,43 @@ +.. _known_issues: + +Known issues +############ + +.. contents:: + :local: + :depth: 3 + +Known issues listed on this page *and* tagged with the :ref:`latest release version ` are valid for the current state of development. +Use the drop-down filter to see known issues for previous releases and check if they are still valid. + +A known issue can list one or both of the following entries: + +* **Affected platforms:** + + If a known issue does not have any specific platforms listed, it is valid for all hardware platforms. + +* **Workaround:** + + Some known issues have a workaround. + Sometimes, they are discovered later and added over time. + +.. version-filter:: + :default: v0-2-0 + :container: dl/dt + :tags: [("wontfix", "Won't fix")] + +.. page-filter:: + :name: issues + + wontfix Won't fix + +List of known issues +******************** + +.. rst-class:: v0-1-0 + +This is the name of a known issue + And this is its description. + At least one line of description must exist for everything to work correctly. + + **Workaround:** There is no workaround. diff --git a/docs/links.txt b/docs/links.txt index 06ba0418..929bff6d 100644 --- a/docs/links.txt +++ b/docs/links.txt @@ -142,7 +142,7 @@ .. _`ZBOSS stack release notes`: .. _`external ZBOSS development guide and API documentation`: https://nrfconnect.github.io/ncs-zigbee/zboss/4.1.4.2/zigbee_devguide.html .. _`Stack commissioning start sequence`: https://nrfconnect.github.io/ncs-zigbee/zboss/4.1.4.2/using_zigbee__z_c_l.html#stack_start_initiation -.. _`ZBOSS NCP Host`: https://github.com/nrfconnect/ncs-zigbee/resources/ncp_host_v3.0.0.zip +.. _`ZBOSS NCP Host`: https://github.com/nrfconnect/ncs-zigbee/raw/refs/heads/main/resources/ncp_host_v3.0.0.zip .. _`NCP Host documentation`: https://nrfconnect.github.io/ncs-zigbee/zboss/4.1.4.2/zboss_ncp_host_intro.html .. _`Rebuilding the ZBOSS libraries for host`: https://nrfconnect.github.io/ncs-zigbee/zboss/4.1.4.2/zboss_ncp_host.html#rebuilding_libs .. _`process the frame`: https://nrfconnect.github.io/ncs-zigbee/zboss/4.1.4.2/using_zigbee__z_c_l.html#process_zcl_cmd diff --git a/docs/release-notes.rst b/docs/release-notes.rst index bc9af434..fd888863 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,4 +1,4 @@ -.. _example_release_notes: +.. _release_notes: Release notes ############# @@ -16,15 +16,13 @@ See also the `Release notes for the nRF Connect SDK`_ and the :ref:`zboss_change The |addon| v\ |addon_version| is compatible with |NCS| v\ |ncs_version| and uses the ZBOSS stack version |zboss_version|. For a full list of |addon|, related |NCS| and ZBOSS stack and NCP host package versions, view the following table: -.. toggle:: - - +-------------------+------------------+-----------------------+---------------------+ - | |addon| version | |NCS| version | ZBOSS stack version | NCP host version | - +===================+==================+=======================+=====================+ - | 0.2.0 | 2.8.0 | 4.1.4.2 | 3.0.0 | - +-------------------+ + +---------------------+ - | 0.1.0 | | | N/A | - +-------------------+------------------+-----------------------+---------------------+ ++-------------------+------------------+-----------------------+---------------------+ +| |addon| version | |NCS| version | ZBOSS stack version | NCP host version | ++===================+==================+=======================+=====================+ +| 0.2.0 | 2.8.0 | 4.1.4.2 | 3.0.0 | ++-------------------+ + +---------------------+ +| 0.1.0 | | | N/A | ++-------------------+------------------+-----------------------+---------------------+ .. _zigbee_release: @@ -32,9 +30,9 @@ For a full list of |addon|, related |NCS| and ZBOSS stack and NCP host package v *************************** This is an experimental release. - + * Added: - + * The :ref:`NCP ` sample. * The `ZBOSS NCP Host`_ package v\ |zigbee_ncp_package_version|. diff --git a/docs/versions.json b/docs/versions.json new file mode 100644 index 00000000..434e9e81 --- /dev/null +++ b/docs/versions.json @@ -0,0 +1,4 @@ +[ + "0.2.0", + "0.1.0" +]