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), }