Commit f2f14961 authored by Benjamin Cabé's avatar Benjamin Cabé Committed by Anas Nashif
Browse files

doc: Introduce boards catalog



This commit adds support for generating an interactive catalog of all
the supported boards that can be included in the documentation using
the `.. zephyr:board-catalog::` directive.

Signed-off-by: default avatarBenjamin Cabé <benjamin@zephyrproject.org>
parent 4eb415e3
Loading
Loading
Loading
Loading
+12 −6
Original line number Diff line number Diff line
.. _boards:

Supported Boards
################

Zephyr project developers are continually adding board-specific support as
documented below.
Supported Boards and Shields
############################

If you are looking to add Zephyr support for a new board, please start with the
:ref:`board_porting_guide`.

When adding support documentation for each board, remember to use the template
When adding support documentation for a board, remember to use the template
available under :zephyr_file:`doc/templates/board.tmpl`.

Shields are hardware add-ons that can be stacked on top of a board to add extra
functionality. They are listed separately from boards, towards :ref:`the end of
this page <boards-shields>`.

Use the interactive search form below to quickly navigate through the list of
supported boards.

.. toctree::
   :maxdepth: 2
   :glob:
   :hidden:

   */index

.. zephyr:board-catalog::

.. _boards-shields:

Shields
+41 −1
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ Directives
- ``zephyr:code-sample::`` - Defines a code sample.
- ``zephyr:code-sample-category::`` - Defines a category for grouping code samples.
- ``zephyr:code-sample-listing::`` - Shows a listing of code samples found in a given category.
- ``zephyr:board-catalog::`` - Show a listing of boards supported by Zephyr.

Roles
-----
@@ -23,9 +24,10 @@ Roles

"""

import sys
from os import path
from pathlib import Path
from typing import Any, Dict, Iterator, List, Tuple
from typing import Any, Dict, Iterator, List, Tuple, Final

from docutils import nodes
from docutils.parsers.rst import Directive, directives
@@ -42,6 +44,7 @@ from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.nodes import NodeMatcher, make_refnode
from sphinx.util.parsing import nested_parse_to_nodes
from sphinx.util.template import SphinxRenderer

from zephyr.doxybridge import DoxygenGroupDirective
from zephyr.gh_utils import gh_link_get_url
@@ -53,6 +56,14 @@ from anytree import Node, Resolver, ChildResolverError, PreOrderIter, search

__version__ = "0.2.0"

ZEPHYR_BASE = Path(__file__).parents[4]

sys.path.insert(0, str(ZEPHYR_BASE / "scripts/dts/python-devicetree/src"))
sys.path.insert(0, str(Path(__file__).parents[3] / "_scripts"))

from gen_boards_catalog import get_catalog

TEMPLATES_DIR = Path(__file__).parent / "templates"
RESOURCES_DIR = Path(__file__).parent / "static"

logger = logging.getLogger(__name__)
@@ -558,6 +569,25 @@ class CodeSampleListingDirective(SphinxDirective):
        return [code_sample_listing_node]


class BoardCatalogDirective(SphinxDirective):
    has_content = False
    required_arguments = 0
    optional_arguments = 0

    def run(self):
        if self.env.app.builder.format == "html":
            self.env.domaindata["zephyr"]["has_board_catalog"][self.env.docname] = True

            # As it is not expected that more than one board-catalog directive is used across
            # the documentation, and since the generation is only taking a few seconds,  we don't
            # store the catalog in the domain data. It might change in the future if the generation
            # becomes more expensive.
            board_catalog = get_catalog()
            renderer = SphinxRenderer([TEMPLATES_DIR])
            rendered = renderer.render("board-catalog.html", {"catalog": board_catalog})
            return [nodes.raw("", rendered, format="html")]


class ZephyrDomain(Domain):
    """Zephyr domain"""

@@ -573,6 +603,7 @@ class ZephyrDomain(Domain):
        "code-sample": CodeSampleDirective,
        "code-sample-listing": CodeSampleListingDirective,
        "code-sample-category": CodeSampleCategoryDirective,
        "board-catalog": BoardCatalogDirective,
    }

    object_types: Dict[str, ObjType] = {
@@ -586,6 +617,7 @@ class ZephyrDomain(Domain):
        "code-samples-categories-tree": Node("samples"),
        # keep track of documents containing special directives
        "has_code_sample_listing": {},  # docname -> bool
        "has_board_catalog": {},  # docname -> bool
    }

    def clear_doc(self, docname: str) -> None:
@@ -604,6 +636,7 @@ class ZephyrDomain(Domain):
        # TODO clean up the anytree as well

        self.data["has_code_sample_listing"].pop(docname, None)
        self.data["has_board_catalog"].pop(docname, None)

    def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
        self.data["code-samples"].update(otherdata["code-samples"])
@@ -626,6 +659,10 @@ class ZephyrDomain(Domain):
            self.data["has_code_sample_listing"][docname] = otherdata[
                "has_code_sample_listing"
            ].get(docname, False)
            self.data["has_board_catalog"][docname] = otherdata["has_board_catalog"].get(
                docname, False
            )

    def get_objects(self):
        for _, code_sample in self.data["code-samples"].items():
            yield (
@@ -761,6 +798,9 @@ def install_static_assets_as_needed(
        app.add_js_file("js/codesample-livesearch.js")

    if app.env.domaindata["zephyr"]["has_board_catalog"].get(pagename, False):
        app.add_css_file("css/board-catalog.css")
        app.add_js_file("js/board-catalog.js")


def setup(app):
    app.add_config_value("zephyr_breathe_insert_related_samples", False, "env")
+214 −0
Original line number Diff line number Diff line
/**
 * Copyright (c) 2024, The Linux Foundation.
 * SPDX-License-Identifier: Apache-2.0
 */

.hidden {
  display: none !important;
}

.filter-form {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 20px;
}

.filter-form input,
.filter-form select {
  appearance: none;
  font-family: var(--system-font-family);
  font-size: 14px;
  border-radius: 50px;
  padding: 10px 18px;
  flex: 1 1 200px;
  background-color: var(--input-background-color);
  color: var(--body-color);
  transition: all 0.3s ease;
  box-shadow: none;
}

.filter-form input:focus .filter-form select:focus {
  border-color: var(--input-focus-border-color);
}
.select-container {
  flex: 1 1 200px;
  position: relative;
}

.select-container::after {
  content: "\25BC";
  position: absolute;
  right: 20px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  font-size: 14px;
  color: var(--body-color);
}

.filter-form select {
  padding-right: 40px;
  width: 100%;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}

#catalog {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  justify-content: center;
  margin-top: 20px;
  margin-bottom: 40px;
}

.board-card {
  flex: 1 1 calc(33.3% - 20px);
  /* Three cards per row */
  max-width: calc(33.3% - 20px);
  border-radius: 8px;
  padding: 15px 20px;
  background-color: var(--admonition-note-background-color);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  align-items: center;
  transition: transform 0.3s ease;
}

.board-card:hover,
.board-card:focus {
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
  transform: translateY(-5px);
  text-decoration: none;
}

.board-card .picture {
  width: auto;
  height: auto;
  min-height: 100px;
  max-height: 180px;
  border-radius: 4px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  flex-grow: 1;
  padding: 10px 0px;
}

.board-card .no-picture {
  font-size: 5em;
  color: var(--admonition-note-title-background-color);
  justify-content: center;
}

.board-card .vendor {
  font-size: 12px;
  color: var(--admonition-note-color);
  font-weight: 900;
  margin-bottom: 18px;
  opacity: 0.5;
}

.board-card img {
  max-height: 100%;
  max-width: 100%;
  object-fit: contain;
}

.board-card .board-name {
  font-family: var(--header-font-family);
  margin: auto 0 5px;
  text-align: center;
  font-size: 18px;
  font-weight: 500;
  color: var(--body-color);
  padding-top: 10px;
}

.board-card .arch {
  margin: 5px 0;
  text-align: center;
  font-size: 12px;
  font-weight: 100;
  color: var(--body-color);
}

@media (max-width: 1024px) {
  .board-card {
    flex: 1 1 calc(50% - 20px);
    max-width: calc(50% - 20px);
  }
}

@media (max-width: 768px) {
  .board-card {
    flex: 1 1 calc(100% - 20px);
    max-width: calc(100% - 20px);
  }

  .board-card .picture {
    min-height: 60px;
    max-height: 120px;
  }
}

#form-options .btn {
  font-size: 14px;
}

#form-options .btn:focus {
  outline-color: var(--body-color) !important;
}

#catalog.compact {
  display: block;
  list-style-type: disc;
  margin: 20px;
}

#catalog.compact .board-card {
  display: list-item;
  padding: 4px 0;
  border: none;
  background-color: transparent;
  box-shadow: none;
  list-style-position: outside;
  max-width: none;
}

#catalog.compact .board-card .vendor,
#catalog.compact .board-card .picture {
  display: none;
}

#catalog.compact .board-card .board-name {
  display: inline;
  font-family: var(--system-font-family);
  text-align: left;
  font-size: 16px;
  font-weight: normal;
  color: var(--body-color);
  margin: 0;
  padding: 0;
}

#catalog.compact .board-card .arch {
  display: inline;
}

#catalog.compact .board-card .arch::before {
  content: " (";
}

#catalog.compact .board-card .arch::after {
  content: ")";
}

#catalog.compact .board-card:hover {
  box-shadow: none;
  transform: none;
  text-decoration: underline;
}
 No newline at end of file
+111 −0
Original line number Diff line number Diff line
/**
 * Copyright (c) 2024, The Linux Foundation.
 * SPDX-License-Identifier: Apache-2.0
 */

function toggleDisplayMode(btn) {
  const catalog = document.getElementById("catalog");
  catalog.classList.toggle("compact");
  btn.classList.toggle("fa-bars");
  btn.classList.toggle("fa-th");
  btn.textContent = catalog.classList.contains("compact")
    ? " Switch to Card View"
    : " Switch to Compact View";
}

function populateFormFromURL() {
  const params = ["name", "arch", "vendor"];
  const hashParams = new URLSearchParams(window.location.hash.slice(1));
  params.forEach((param) => {
    const element = document.getElementById(param);
    if (hashParams.has(param)) {
      element.value = hashParams.get(param);
    }
  });

  filterBoards();
}

function updateURL() {
  const params = ["name", "arch", "vendor"];
  const hashParams = new URLSearchParams(window.location.hash.slice(1));

  params.forEach((param) => {
    const value = document.getElementById(param).value;
    value ? hashParams.set(param, value) : hashParams.delete(param);
  });

  window.history.replaceState({}, "", `#${hashParams.toString()}`);
}

document.addEventListener("DOMContentLoaded", function () {
  updateBoardCount();
  populateFormFromURL();

  const form = document.querySelector(".filter-form");

  // sort vendors alphabetically
  vendorSelect = document.getElementById("vendor");
  vendorOptions = Array.from(vendorSelect.options).slice(1);
  vendorOptions.sort((a, b) => a.text.localeCompare(b.text));
  while (vendorSelect.options.length > 1) {
    vendorSelect.remove(1);
  }
  vendorOptions.forEach((option) => {
    vendorSelect.appendChild(option);
  });

  form.addEventListener("submit", function (event) {
    event.preventDefault();
  });

  form.addEventListener("input", function () {
    filterBoards();
    updateURL();
  });
});

function updateBoardCount() {
  const boards = document.getElementsByClassName("board-card");
  const visibleBoards = Array.from(boards).filter(
    (board) => !board.classList.contains("hidden")
  ).length;
  const totalBoards = boards.length;
  document.getElementById("nb-matches").textContent = `Showing ${visibleBoards} of ${totalBoards}`;
}

function filterBoards() {
  const nameInput = document.getElementById("name").value.toLowerCase();
  const archSelect = document.getElementById("arch").value;
  const vendorSelect = document.getElementById("vendor").value;

  const resetFiltersBtn = document.getElementById("reset-filters");
  if (nameInput || archSelect || vendorSelect) {
    resetFiltersBtn.classList.remove("btn-disabled");
  } else {
    resetFiltersBtn.classList.add("btn-disabled");
  }

  const boards = document.getElementsByClassName("board-card");

  Array.from(boards).forEach(function (board) {
    const boardName = board.getAttribute("data-name").toLowerCase();
    const boardArchs = board.getAttribute("data-arch").split(" ");
    const boardVendor = board.getAttribute("data-vendor");

    let matches = true;

    matches =
      !(nameInput && !boardName.includes(nameInput)) &&
      !(archSelect && !boardArchs.includes(archSelect)) &&
      !(vendorSelect && boardVendor !== vendorSelect);

    if (matches) {
      board.classList.remove("hidden");
    } else {
      board.classList.add("hidden");
    }
  });

  updateBoardCount();
}
+26 −0
Original line number Diff line number Diff line
{#
  Copyright (c) 2024, The Linux Foundation.
  SPDX-License-Identifier: Apache-2.0
#}

<a
  class="board-card"
  {% if board.doc_page %}
  href="../{{ board.doc_page | replace(".rst", ".html") }}"
  {% else %}
  href="#"
  {% endif %}
  aria-label="Open the documentation page for {{ board.full_name }}"
  data-name="{{ board.full_name}}"
  data-arch="{{ board.archs | join(" ") }}"
  data-vendor="{{ board.vendor }}"
  tabindex="0">
  <div class="vendor">{{ catalog.vendors[board.vendor] }}</div>
  {% if board.image %}
  <img alt="A picture of the {{ board.name }} board" src="{{ board.image }}" class="picture" />
  {% else %}
  <div class="no-picture fa fa-microchip picture"></div>
  {% endif %}
  <div class="board-name">{{ board.full_name }}</div>
  <div class="arch">{{ board.archs | join(", ") }}</div>
</a>
Loading