Skip to content

Commit

Permalink
pw_docgen: Refactor metadata extension
Browse files Browse the repository at this point in the history
- Allow custom nav buttons
- Show nav on all module doc subpages

Bug: 280593975
Change-Id: I6afc1b37d2e6907cbbeb6bc80b6d0d997f67c919
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/144890
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Kayce Basques <kayce@google.com>
Commit-Queue: Chad Norvell <chadnorvell@google.com>
  • Loading branch information
chadnorvell authored and CQ Bot Account committed May 5, 2023
1 parent 513be82 commit f92c560
Show file tree
Hide file tree
Showing 17 changed files with 254 additions and 101 deletions.
214 changes: 133 additions & 81 deletions pw_docgen/py/pw_docgen/sphinx/module_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,40 @@
# the License.
"""Sphinx directives for Pigweed module metadata"""

from typing import List
from typing import cast, Dict, List

import docutils
from docutils import nodes
import docutils.statemachine

# pylint: disable=consider-using-from-import
import docutils.parsers.rst.directives as directives # type: ignore

# pylint: enable=consider-using-from-import
from sphinx.application import Sphinx as SphinxApplication
from sphinx.util.docutils import SphinxDirective
from sphinx_design.badges_buttons import ButtonRefDirective # type: ignore
from sphinx_design.cards import CardDirective # type: ignore
from sphinx_design.badges_buttons import ButtonRefDirective
from sphinx_design.cards import CardDirective


def status_choice(arg):
return directives.choice(arg, ('experimental', 'unstable', 'stable'))


def status_badge(module_status: str) -> str:
"""Given a module status, return the status badge for rendering."""
role = ':bdg-primary:'
return role + f'`{module_status.title()}`'


def cs_url(module_name: str):
def cs_url(module_name: str) -> str:
"""Return the codesearch URL for the given module."""
return f'https://cs.opensource.google/pigweed/pigweed/+/main:{module_name}/'


def concat_tags(*tag_lists: List[str]):
def concat_tags(*tag_lists: List[str]) -> List[str]:
"""Given a list of tag lists, return them concat'ed and ready for render."""

all_tags = tag_lists[0]

for tag_list in tag_lists[1:]:
Expand All @@ -51,6 +57,47 @@ def concat_tags(*tag_lists: List[str]):
return all_tags


def parse_nav(nav_option: str) -> Dict[str, str]:
"""Parse the `nav` option.
The value for this option must contain key-value pairs separated by ':',
with each pair on a separate line. The value is a Sphinx ref pointing to
the destination of the nav button link, and the key is the name of the nav
button (see :func:`nav_name`).
"""
try:
return {
line.split(':')[0].strip(): line.split(':')[1].strip()
for line in nav_option.split('\n')
}
except IndexError:
raise ValueError(
'Nav parsing failure. Ensure each line has a single pair in the '
f'form `name: ref`. You provided this:\n{nav_option}'
)


def nav_name(name: str) -> str:
"""Return the display name for a particular nav button name.
By default, the returned display name be the provided name with the words
capitalized. However, this can be overridden by adding an entry to the
canonical names list.
"""
canonical_names: Dict[str, str] = {
'api': 'API Reference',
'cli': 'CLI Reference',
'gui': 'GUI Reference',
}

if name in canonical_names:
return canonical_names[name]

return ' '.join(
[name_part.lower().capitalize() for name_part in name.split()]
)


class PigweedModuleDirective(SphinxDirective):
"""Directive registering module metadata, rendering title & info card."""

Expand All @@ -65,28 +112,25 @@ class PigweedModuleDirective(SphinxDirective):
'languages': directives.unchanged,
'code-size-impact': directives.unchanged,
'facade': directives.unchanged,
'get-started': directives.unchanged_required,
'tutorials': directives.unchanged,
'guides': directives.unchanged,
'concepts': directives.unchanged,
'design': directives.unchanged_required,
'api': directives.unchanged,
'nav': directives.unchanged_required,
}

def try_get_option(self, option: str):
def _try_get_option(self, option: str):
"""Try to get an option by name and raise on failure."""

try:
return self.options[option]
except KeyError:
raise self.error(f' :{option}: option is required')

def maybe_get_option(self, option: str):
try:
return self.options[option]
except KeyError:
return None
def _maybe_get_option(self, option: str):
"""Try to get an option by name and return None on failure."""
return self.options.get(option, None)

def create_nav_button(self, title: str, ref: str) -> nodes.list_item:
"""Generate a renderable node for a nav button."""

def create_section_button(self, title: str, ref: str):
node = docutils.nodes.list_item(classes=['pw-module-section-button'])
node = nodes.list_item(classes=['pw-module-section-button'])
node += ButtonRefDirective(
name='',
arguments=[ref],
Expand All @@ -101,38 +145,14 @@ def create_section_button(self, title: str, ref: str):

return node

def register_metadata(self):
module_name = self.try_get_option('name')

if 'facade' in self.options:
facade = self.options['facade']

# Initialize the module relationship dict if needed
if not hasattr(self.env, 'pw_module_relationships'):
self.env.pw_module_relationships = {}

# Initialize the backend list for this facade if needed
if facade not in self.env.pw_module_relationships:
self.env.pw_module_relationships[facade] = []

# Add this module as a backend of the provided facade
self.env.pw_module_relationships[facade].append(module_name)

if 'is-deprecated' in self.options:
# Initialize the deprecated modules list if needed
if not hasattr(self.env, 'pw_modules_deprecated'):
self.env.pw_modules_deprecated = []

self.env.pw_modules_deprecated.append(module_name)

def run(self):
tagline = docutils.nodes.paragraph(
text=self.try_get_option('tagline'),
def run(self) -> List[nodes.Node]:
tagline = nodes.paragraph(
classes=['section-subtitle'],
text=self._try_get_option('tagline'),
)

status_tags: List[str] = [
status_badge(self.try_get_option('status')),
status_badge(self._try_get_option('status')),
]

if 'is-deprecated' in self.options:
Expand All @@ -149,13 +169,14 @@ def run(self):

code_size_impact = []

if code_size_text := self.maybe_get_option('code-size-impact'):
if code_size_text := self._maybe_get_option('code-size-impact'):
code_size_impact.append(f'**Code Size Impact:** {code_size_text}')

# Move the directive content into a section that we can render wherever
# we want.
content = docutils.nodes.paragraph()
self.state.nested_parse(self.content, 0, content)
raw_content = cast(List[str], self.content) # type: ignore
content = nodes.paragraph()
self.state.nested_parse(raw_content, 0, content)

# The card inherits its content from this node's content, which we've
# already pulled out. So we can replace this node's content with the
Expand All @@ -170,55 +191,86 @@ def run(self):
options={},
)

try:
nav = parse_nav(self._try_get_option('nav'))
except ValueError as err:
raise self.error(err)

# Create the top-level section buttons.
section_buttons = docutils.nodes.bullet_list(
section_buttons = nodes.bullet_list(
classes=['pw-module-section-buttons']
)

# This is the pattern for required sections.
section_buttons += self.create_section_button(
'Get Started', self.try_get_option('get-started')
)
for name, ref in nav.items():
nav_button = self.create_nav_button(nav_name(name), ref)
section_buttons += nav_button

# This is the pattern for optional sections.
if (tutorials_ref := self.maybe_get_option('tutorials')) is not None:
section_buttons += self.create_section_button(
'Tutorials', tutorials_ref
)
return [
tagline,
section_buttons,
content,
card,
]


class PigweedModuleSubpageDirective(PigweedModuleDirective):
"""Directive registering module metadata, rendering title & info card."""

if (guides_ref := self.maybe_get_option('guides')) is not None:
section_buttons += self.create_section_button('Guides', guides_ref)
required_arguments = 0
final_argument_whitespace = True
has_content = True
option_spec = {
'name': directives.unchanged_required,
'tagline': directives.unchanged_required,
'nav': directives.unchanged_required,
}

def run(self) -> List[nodes.Node]:
tagline = nodes.paragraph(
classes=['section-subtitle'],
text=self._try_get_option('tagline'),
)

if (concepts_ref := self.maybe_get_option('concepts')) is not None:
section_buttons += self.create_section_button(
'Concepts', concepts_ref
)
# Move the directive content into a section that we can render wherever
# we want.
raw_content = cast(List[str], self.content) # type: ignore
content = nodes.paragraph()
self.state.nested_parse(raw_content, 0, content)

section_buttons += self.create_section_button(
'Design', self.try_get_option('design')
card = CardDirective.create_card(
inst=self,
arguments=[],
options={},
)

if (api_ref := self.maybe_get_option('api')) is not None:
section_buttons += self.create_section_button(
'API Reference', api_ref
)
try:
nav = parse_nav(self._try_get_option('nav'))
except ValueError as err:
raise self.error(err)

return [tagline, section_buttons, content, card]
# Create the top-level section buttons.
section_buttons = nodes.bullet_list(
classes=['pw-module-section-buttons']
)

nav_buttons: List[nodes.list_item] = []

def build_backend_lists(app, _doctree, _fromdocname):
env = app.builder.env
for name, ref in nav.items():
nav_button = self.create_nav_button(nav_name(name), ref)
section_buttons += nav_button
nav_buttons.append(nav_button)

if not hasattr(env, 'pw_module_relationships'):
env.pw_module_relationships = {}
return [
tagline,
section_buttons,
content,
card,
]


def setup(app: SphinxApplication):
app.add_directive('pigweed-module', PigweedModuleDirective)

# At this event, the documents and metadata have been generated, and now we
# can modify the doctree to reflect the metadata.
app.connect('doctree-resolved', build_backend_lists)
app.add_directive('pigweed-module-subpage', PigweedModuleSubpageDirective)

return {
'parallel_read_safe': True,
Expand Down
9 changes: 9 additions & 0 deletions pw_hdlc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
======================
pw_hdlc: API reference
======================
.. pigweed-module-subpage::
:name: pw_hdlc
:tagline: Lightweight, simple, and easy serial communication
:nav:
getting started: module-pw_hdlc-get-started
design: module-pw_hdlc-design
guides: module-pw_hdlc-guide
api: module-pw_hdlc-api

This page describes the :ref:`module-pw_hdlc-api-encoder`, :ref:`module-pw_hdlc-api-decoder`,
and :ref:`module-pw_hdlc-api-rpc` APIs of ``pw_hdlc``.

Expand Down
8 changes: 8 additions & 0 deletions pw_hdlc/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
===============
pw_hdlc: Design
===============
.. pigweed-module-subpage::
:name: pw_hdlc
:tagline: Lightweight, simple, and easy serial communication
:nav:
getting started: module-pw_hdlc-get-started
design: module-pw_hdlc-design
guides: module-pw_hdlc-guide
api: module-pw_hdlc-api

``pw_hdlc`` implements a subset of the
`HDLC <https://en.wikipedia.org/wiki/High-Level_Data_Link_Control>`_
Expand Down
10 changes: 5 additions & 5 deletions pw_hdlc/docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
=======
pw_hdlc
=======

.. pigweed-module::
:name: pw_hdlc
:tagline: Lightweight, simple, and easy serial communication
:status: stable
:languages: C++17
:code-size-impact: 1400 to 2600 bytes
:get-started: module-pw_hdlc-get-started
:design: module-pw_hdlc-design
:guides: module-pw_hdlc-guide
:api: module-pw_hdlc-api
:nav:
getting started: module-pw_hdlc-get-started
design: module-pw_hdlc-design
guides: module-pw_hdlc-guide
api: module-pw_hdlc-api

- Transmit RPCs and other data between devices over serial
- Detect corruption and data loss
Expand Down
9 changes: 9 additions & 0 deletions pw_hdlc/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
=====================
pw_hdlc: How-to guide
=====================
.. pigweed-module-subpage::
:name: pw_hdlc
:tagline: Lightweight, simple, and easy serial communication
:nav:
getting started: module-pw_hdlc-get-started
design: module-pw_hdlc-design
guides: module-pw_hdlc-guide
api: module-pw_hdlc-api

This page shows you how to use the :ref:`module-pw_hdlc-api-encoder` and
:ref:`module-pw_hdlc-api-decoder` APIs of :ref:`module-pw_hdlc`.

Expand Down
8 changes: 8 additions & 0 deletions pw_software_update/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
---------------------------------
pw_software_update: CLI reference
---------------------------------
.. pigweed-module-subpage::
:name: pw_software_update
:tagline: Secure software delivery
:nav:
getting started: module-pw_software_update-get-started
design: module-pw_software_update-design
guides: module-pw_software_update-guides
cli: module-pw_software_update-cli

Overview
---------
Expand Down
Loading

0 comments on commit f92c560

Please sign in to comment.