From f6d11a51e8f024eade5e2aa655cede9798c8bd85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Cab=C3=A9?= Date: Tue, 25 Mar 2025 09:42:53 +0100 Subject: [PATCH] doc: _extensions: add board target selector for supported HW features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This show a nice widget to switch between the various board targets and see their respective list of supported features. The original HTML content of the page is preserved and JavaScript code "patches" the page on-the-fly. This is so that the actual HTML content (that e.g. search engines see) is complete and indexable (as well as to provide useful information for folks who might have JavaScript disabled altogether). Signed-off-by: Benjamin Cabé --- doc/_extensions/zephyr/domain/__init__.py | 30 ++- .../zephyr/domain/static/css/board.css | 75 +++++- .../zephyr/domain/static/js/board.js | 244 +++++++++++++++++- doc/_scripts/gen_boards_catalog.py | 1 + 4 files changed, 344 insertions(+), 6 deletions(-) diff --git a/doc/_extensions/zephyr/domain/__init__.py b/doc/_extensions/zephyr/domain/__init__.py index 8525acaadd7..ab2edf4cd5b 100644 --- a/doc/_extensions/zephyr/domain/__init__.py +++ b/doc/_extensions/zephyr/domain/__init__.py @@ -724,6 +724,7 @@ class BoardDirective(SphinxDirective): board_node = BoardNode(id=board_name) board_node["full_name"] = board["full_name"] board_node["vendor"] = vendors.get(board["vendor"], board["vendor"]) + board_node["revision_default"] = board["revision_default"] board_node["supported_features"] = board["supported_features"] board_node["archs"] = board["archs"] board_node["socs"] = board["socs"] @@ -825,18 +826,39 @@ class BoardSupportedHardwareDirective(SphinxDirective): """ result_nodes.append(nodes.raw("", html_contents, format="html")) + tables_container = nodes.container(ids=[f"{board_node['id']}-hw-features"]) + result_nodes.append(tables_container) + + board_json = json.dumps( + { + "board_name": board_node["id"], + "revision_default": board_node["revision_default"], + "targets": list(supported_features.keys()), + } + ) + result_nodes.append( + nodes.raw( + "", + f"""""", + format="html", + ) + ) + for target, features in sorted(supported_features.items()): if not features: continue - target_heading = nodes.section(ids=[f"{board_node['id']}-{target}-hw-features"]) + target_heading = nodes.section(ids=[f"{board_node['id']}-{target}-hw-features-section"]) heading = nodes.title() heading += nodes.literal(text=target) heading += nodes.Text(" target") target_heading += heading - result_nodes.append(target_heading) + tables_container += target_heading - table = nodes.table(classes=["colwidths-given", "hardware-features"]) + table = nodes.table( + classes=["colwidths-given", "hardware-features"], + ids=[f"{board_node['id']}-{target}-hw-features-table"], + ) tgroup = nodes.tgroup(cols=4) tgroup += nodes.colspec(colwidth=15, classes=["type"]) @@ -965,7 +987,7 @@ class BoardSupportedHardwareDirective(SphinxDirective): tgroup += tbody table += tgroup - result_nodes.append(table) + tables_container += table return result_nodes diff --git a/doc/_extensions/zephyr/domain/static/css/board.css b/doc/_extensions/zephyr/domain/static/css/board.css index c5fbc205d4d..d2d90b9fe5c 100644 --- a/doc/_extensions/zephyr/domain/static/css/board.css +++ b/doc/_extensions/zephyr/domain/static/css/board.css @@ -105,6 +105,79 @@ } } +.board-target-selector { + margin-bottom: 1em; + padding: 15px; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + position: relative; +} + +.board-target-selector > div { + display: flex; + flex-direction: column; + gap: 5px; +} + +.static-value { + padding: 5px 0px; + color: var(--body-color); + font-family: var(--monospace-font-family); + font-size: 0.9em; +} + +.separator { + font-size: 16px; + font-weight: bold; + margin: 0 4px; +} + +.board-target-selector select { + background-color: var(--input-background-color); + color: var(--body-color); + border-radius: 4px; + font-family: var(--monospace-font-family); + font-size: 0.9em; + box-shadow: none; + transition: none; +} + +.board-target-selector select:focus { + border-color: var(--input-focus-border-color); +} + +.copy-button { + background: transparent; + border: 1px solid var(--input-border-color); + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 0.8em; + color: var(--body-color); + display: flex; + align-items: center; + gap: 4px; + transition: all 0.2s ease; +} + +.copy-button:hover { + background: var(--input-background-color); + border-color: var(--input-focus-border-color); +} + +.copy-button.copied { + background: var(--admonition-note-title-background-color); + color: var(--admonition-note-title-color); + border-color: var(--admonition-note-title-color); +} + +.copy-button svg { + width: 18px; + height: 18px; + fill: currentColor; +} .hardware-features { th { @@ -221,4 +294,4 @@ display: flex; align-items: center; gap: 6px; -} \ No newline at end of file +} diff --git a/doc/_extensions/zephyr/domain/static/js/board.js b/doc/_extensions/zephyr/domain/static/js/board.js index a748d64ef2b..6ff36a5b0f2 100644 --- a/doc/_extensions/zephyr/domain/static/js/board.js +++ b/doc/_extensions/zephyr/domain/static/js/board.js @@ -3,4 +3,246 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* file intentionally left blank */ +(() => { + // SVG icons for copy button + const COPY_ICON = ``; + const CHECK_ICON = ``; + + // DOM element creation helper + const createElement = (tag, className, text) => { + const element = document.createElement(tag); + if (className) element.className = className; + if (text) element.textContent = text; + return element; + }; + + // Target parsing helper + const parseTargetString = (targetString) => { + const [boardWithRev, ...qualifiers] = targetString.split('/'); + const [board, revision] = boardWithRev.split('@'); + return { + board: board || '', + revision: revision || '', + qualifier: qualifiers.join('/') || '' + }; + }; + + // Select element creation helper + const createSelect = (options, selectedValue) => { + const select = createElement('select'); + options.sort().forEach(value => { + const option = createElement('option', null, value); + option.value = value; + option.selected = (value === selectedValue); + select.appendChild(option); + }); + return select; + }; + + // Main initialization function + const initializeBoardSelector = () => { + const container = document.querySelector('.container[id$="-hw-features"]'); + if (!container) return console.error('Container element not found'); + + const components = { + boards: new Set([board_data.board_name]), + revisions: new Set(), + qualifiers: new Set() + }; + + board_data.targets.forEach(target => { + const { revision, qualifier } = parseTargetString(target); + if (revision) components.revisions.add(revision); + if (qualifier) components.qualifiers.add(qualifier); + }); + + const initialValues = parseTargetString(board_data.targets[0]); + if (board_data.revision_default) { + initialValues.revision = board_data.revision_default; + } + + const selector = createElement('div', 'board-target-selector'); + selector.appendChild(createElement('div', 'static-value', initialValues.board)); + + // Add revision selector if needed + let revisionSelect; + if (components.revisions.size > 0) { + selector.appendChild(createElement('div', 'separator', '@')); + revisionSelect = createSelect( + Array.from(components.revisions), + initialValues.revision + ); + selector.appendChild(revisionSelect); + } + + // Add qualifier selector or static value + let qualifierSelect; + if (components.qualifiers.size > 0) { + selector.appendChild(createElement('div', 'separator', '/')); + if (components.qualifiers.size === 1) { + selector.appendChild(createElement('div', 'static-value', initialValues.qualifier)); + } else { + qualifierSelect = createSelect( + Array.from(components.qualifiers), + null + ); + selector.appendChild(qualifierSelect); + } + } + + // Add copy button + const copyButton = createElement('button', 'copy-button'); + copyButton.innerHTML = COPY_ICON; + copyButton.addEventListener('click', handleCopyButtonClick( + initialValues, + revisionSelect, + qualifierSelect, + copyButton + )); + selector.appendChild(copyButton); + + // Insert selector into DOM and set up event listeners + container.parentNode.insertBefore(selector, container); + selector.querySelectorAll('select').forEach(select => { + select.addEventListener('change', () => { + updateSelectOptions(initialValues, revisionSelect, qualifierSelect); + updateDisplayedTable(revisionSelect, qualifierSelect); + + }); + }); + + // Initial display setup + initializeDisplay(); + // updateDisplayedTable(revisionSelect, qualifierSelect); + }; + + // Copy button click handler + const handleCopyButtonClick = (initialValues, revisionSelect, qualifierSelect, button) => async () => { + try { + const target = buildTargetString(initialValues, revisionSelect, qualifierSelect); + await navigator.clipboard.writeText(target); + + button.classList.add('copied'); + button.innerHTML = CHECK_ICON; + + setTimeout(() => { + button.classList.remove('copied'); + button.innerHTML = COPY_ICON; + }, 1000); + } catch (error) { + console.error('Failed to copy text:', error); + } + }; + + // Build target string from current selections + const buildTargetString = (initialValues, revisionSelect, qualifierSelect) => { + const parts = [initialValues.board]; + + if (revisionSelect) { + parts.push(`@${revisionSelect.value}`); + } else if (initialValues.revision) { + parts.push(`@${initialValues.revision}`); + } + + if (qualifierSelect) { + parts.push(`/${qualifierSelect.value}`); + } else if (initialValues.qualifier) { + parts.push(`/${initialValues.qualifier}`); + } + + return parts.join(''); + }; + + // Update displayed table based on selections + const updateDisplayedTable = (revisionSelect, qualifierSelect) => { + const currentTarget = buildTargetString( + parseTargetString(board_data.targets[0]), + revisionSelect, + qualifierSelect + ); + + console.log(currentTarget); + + document.querySelectorAll('section[id$="-hw-features-section"]').forEach(section => { + // Find the matching target in board_data.targets + const isMatch = section.id.replace(/${board_data.name}/).replace(/-hw-features-section$/, '').endsWith(currentTarget); + + const table = document.querySelector(`table[id="${section.id.replace('-section', '-table')}"]`); + const wrapper = table?.closest('.wy-table-responsive'); + + + if (table) table.style.display = isMatch ? 'table' : 'none'; + if (wrapper) wrapper.style.display = isMatch ? 'block' : 'none'; + }); + }; + + // Initial display setup + const initializeDisplay = () => { + document.querySelectorAll('section[id$="-hw-features-section"]').forEach(section => section.style.display = 'none'); + document.querySelectorAll('table.hardware-features').forEach((table, index) => { + const wrapper = table.closest('.wy-table-responsive'); + table.style.display = index === 0 ? 'table' : 'none'; + if (wrapper) wrapper.style.display = index === 0 ? 'block' : 'none'; + }); + updateDisplayedTable(revisionSelect, qualifierSelect); + }; + + // Update select options based on valid combinations + const updateSelectOptions = (initialValues, revisionSelect, qualifierSelect) => { + if (revisionSelect) { + const currentBoard = initialValues.board; + const validRevisions = new Set(); + + // Find valid revisions for current board + board_data.targets.forEach(target => { + const { board, revision } = parseTargetString(target); + if (board === currentBoard && revision) { + validRevisions.add(revision); + } + }); + + // Update revision select options + Array.from(revisionSelect.options).forEach(option => { + option.disabled = !validRevisions.has(option.value); + if (option.disabled && option.selected) { + // If current selection is invalid, select first valid option + const firstValid = Array.from(revisionSelect.options).find(opt => !opt.disabled); + if (firstValid) firstValid.selected = true; + } + }); + } + + if (qualifierSelect) { + const currentBoard = initialValues.board; + const currentRevision = revisionSelect ? revisionSelect.value : initialValues.revision; + const validQualifiers = new Set(); + + // Find valid qualifiers for current board and revision + board_data.targets.forEach(target => { + const { board, revision, qualifier } = parseTargetString(target); + if (board === currentBoard && + (!revision || revision === currentRevision) && + qualifier) { + validQualifiers.add(qualifier); + } + }); + + // Update qualifier select options + Array.from(qualifierSelect.options).forEach(option => { + option.disabled = !validQualifiers.has(option.value); + if (option.disabled && option.selected) { + // If current selection is invalid, select first valid option + const firstValid = Array.from(qualifierSelect.options).find(opt => !opt.disabled); + if (firstValid) firstValid.selected = true; + } + }); + } + }; + + // Start initialization when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeBoardSelector); + } else { + initializeBoardSelector(); + } +})(); diff --git a/doc/_scripts/gen_boards_catalog.py b/doc/_scripts/gen_boards_catalog.py index fdce5f0777f..02075e2c5d5 100755 --- a/doc/_scripts/gen_boards_catalog.py +++ b/doc/_scripts/gen_boards_catalog.py @@ -325,6 +325,7 @@ def get_catalog(generate_hw_features=False): "vendor": vendor, "archs": list(archs), "socs": list(socs), + "revision_default": board.revision_default, "supported_features": supported_features, "image": guess_image(board), }