SEED: Create Sphinx directive for metadata

Change-Id: I0f9fa9e7b611161b7a2b30818dc0244194f37672
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161517
Commit-Queue: Chad Norvell <chadnorvell@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/docs/conf.py b/docs/conf.py
index ea55906..47e8425 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -43,6 +43,7 @@
     'pw_docgen.sphinx.google_analytics',  # Enables optional Google Analytics
     'pw_docgen.sphinx.kconfig',
     'pw_docgen.sphinx.module_metadata',
+    'pw_docgen.sphinx.seed_metadata',
     'sphinx.ext.autodoc',  # Automatic documentation for Python code
     'sphinx.ext.napoleon',  # Parses Google-style docstrings
     'sphinxarg.ext',  # Automatic documentation of Python argparse
diff --git a/pw_docgen/py/BUILD.gn b/pw_docgen/py/BUILD.gn
index c298850..0f4b511 100644
--- a/pw_docgen/py/BUILD.gn
+++ b/pw_docgen/py/BUILD.gn
@@ -28,6 +28,7 @@
     "pw_docgen/sphinx/google_analytics.py",
     "pw_docgen/sphinx/kconfig.py",
     "pw_docgen/sphinx/module_metadata.py",
+    "pw_docgen/sphinx/seed_metadata.py",
   ]
   pylintrc = "$dir_pigweed/.pylintrc"
   mypy_ini = "$dir_pigweed/.mypy.ini"
diff --git a/pw_docgen/py/pw_docgen/sphinx/seed_metadata.py b/pw_docgen/py/pw_docgen/sphinx/seed_metadata.py
new file mode 100644
index 0000000..9134081
--- /dev/null
+++ b/pw_docgen/py/pw_docgen/sphinx/seed_metadata.py
@@ -0,0 +1,133 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Sphinx directives for Pigweed SEEDs"""
+
+from typing import 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.cards import CardDirective
+
+
+def status_choice(arg) -> str:
+    return directives.choice(
+        arg, ('open_for_comments', 'last_call', 'accepted', 'rejected')
+    )
+
+
+def parse_status(arg) -> str:
+    """Support variations on the status choices.
+
+    For example, you can use capital letters and spaces.
+    """
+
+    return status_choice('_'.join([token.lower() for token in arg.split(' ')]))
+
+
+def status_badge(seed_status: str, badge_status) -> str:
+    """Given a SEED status, return the status badge for rendering."""
+
+    return (
+        ':bdg-primary:'
+        if seed_status == badge_status
+        else ':bdg-secondary-line:'
+    )
+
+
+def cl_link(cl_num):
+    return (
+        f'`pwrev/{cl_num} '
+        '<https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/'
+        f'{cl_num}>`_'
+    )
+
+
+class PigweedSeedDirective(SphinxDirective):
+    """Directive registering & rendering SEED metadata."""
+
+    required_arguments = 0
+    final_argument_whitespace = True
+    has_content = True
+    option_spec = {
+        'number': directives.positive_int,
+        'name': directives.unchanged_required,
+        'status': parse_status,
+        'proposal_date': directives.unchanged_required,
+        'cl': directives.positive_int_list,
+    }
+
+    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 run(self) -> List[nodes.Node]:
+        seed_number = '{:04d}'.format(self._try_get_option('number'))
+        seed_name = self._try_get_option('name')
+        status = self._try_get_option('status')
+        proposal_date = self._try_get_option('proposal_date')
+        cl_nums = self._try_get_option('cl')
+
+        title = (
+            f':fas:`seedling` SEED-{seed_number}: :ref:'
+            f'`{seed_name}<seed-{seed_number}>`\n'
+        )
+
+        self.content = docutils.statemachine.StringList(
+            [
+                ':octicon:`comment-discussion` Status:',
+                f'{status_badge(status, "open_for_comments")}'
+                '`Open for Comments`',
+                ':octicon:`chevron-right`',
+                f'{status_badge(status, "last_call")}`Last Call`',
+                ':octicon:`chevron-right`',
+                f'{status_badge(status, "accepted")}`Accepted`',
+                ':octicon:`kebab-horizontal`',
+                f'{status_badge(status, "rejected")}`Rejected`',
+                '\n',
+                f':octicon:`calendar` Proposal Date: {proposal_date}',
+                '\n',
+                ':octicon:`code-review` CL: ',
+                ', '.join([cl_link(cl_num) for cl_num in cl_nums]),
+            ]
+        )
+
+        card = CardDirective.create_card(
+            inst=self,
+            arguments=[title],
+            options={},
+        )
+
+        return [card]
+
+
+def setup(app: SphinxApplication):
+    app.add_directive('seed', PigweedSeedDirective)
+
+    return {
+        'parallel_read_safe': True,
+        'parallel_write_safe': True,
+    }
diff --git a/seed/0001-the-seed-process.rst b/seed/0001-the-seed-process.rst
index 3b7958b..58182a8 100644
--- a/seed/0001-the-seed-process.rst
+++ b/seed/0001-the-seed-process.rst
@@ -3,22 +3,12 @@
 ======================
 0001: The SEED Process
 ======================
-
-.. card::
-   :fas:`seedling` SEED-0001: :ref:`The SEED Process<seed-0001>`
-
-   :octicon:`comment-discussion` Status:
-   :bdg-secondary-line:`Open for Comments`
-   :octicon:`chevron-right`
-   :bdg-secondary-line:`Last Call`
-   :octicon:`chevron-right`
-   :bdg-primary:`Accepted`
-   :octicon:`kebab-horizontal`
-   :bdg-secondary-line:`Rejected`
-
-   :octicon:`calendar` Proposal Date: 2022-10-31
-
-   :octicon:`code-review` CL: `pwrev/116577 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/116577>`_
+.. seed::
+   :number: 1
+   :name: The SEED Process
+   :status: Accepted
+   :proposal_date: 2022-10-31
+   :cl: 116577
 
 -------
 Summary
diff --git a/seed/0002-template.rst b/seed/0002-template.rst
index 5b34f9f..844c6e1 100644
--- a/seed/0002-template.rst
+++ b/seed/0002-template.rst
@@ -3,22 +3,12 @@
 ===================
 0002: SEED Template
 ===================
-
-.. card::
-   :fas:`seedling` SEED-0002: :ref:`SEED Template<seed-0002>`
-
-   :octicon:`comment-discussion` Status:
-   :bdg-primary:`Open for Comments`
-   :octicon:`chevron-right`
-   :bdg-secondary-line:`Last Call`
-   :octicon:`chevron-right`
-   :bdg-secondary-line:`Accepted`
-   :octicon:`kebab-horizontal`
-   :bdg-secondary-line:`Rejected`
-
-   :octicon:`calendar` Proposal Date: 2022-11-30
-
-   :octicon:`code-review` CL: `pwrev/123090 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/123090>`_
+.. seed::
+   :number: 2
+   :name: SEED Template
+   :status: Open for Comments
+   :proposal_date: 2022-11-30
+   :cl: 123090
 
 -------
 Summary
diff --git a/seed/0101-pigweed.json.rst b/seed/0101-pigweed.json.rst
index b1f5eda..271f0b1 100644
--- a/seed/0101-pigweed.json.rst
+++ b/seed/0101-pigweed.json.rst
@@ -3,22 +3,12 @@
 ==================
 0101: pigweed.json
 ==================
-
-.. card::
-   :fas:`seedling` SEED-0101: :ref:`pigweed.json<seed-0101>`
-
-   :octicon:`comment-discussion` Status:
-   :bdg-secondary-line:`Open for Comments`
-   :octicon:`chevron-right`
-   :bdg-secondary-line:`Last Call`
-   :octicon:`chevron-right`
-   :bdg-primary:`Accepted`
-   :octicon:`kebab-horizontal`
-   :bdg-secondary-line:`Rejected`
-
-   :octicon:`calendar` Proposal Date: 2023-02-06
-
-   :octicon:`code-review` CL: `pwrev/128010 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/128010>`_
+.. seed::
+   :number: 0101
+   :name: pigweed.json
+   :status: Accepted
+   :proposal_date: 2023-02-06
+   :cl: 128010
 
 -------
 Summary
diff --git a/seed/0102-module-docs.rst b/seed/0102-module-docs.rst
index 9cdff64..3e81791 100644
--- a/seed/0102-module-docs.rst
+++ b/seed/0102-module-docs.rst
@@ -3,22 +3,12 @@
 =====================================
 0102: Consistent Module Documentation
 =====================================
-
-.. card::
-   :fas:`seedling` SEED-0102: :ref:`Consistent Module Documentation<seed-0102>`
-
-   :octicon:`comment-discussion` Status:
-   :bdg-secondary-line:`Open for Comments`
-   :octicon:`chevron-right`
-   :bdg-secondary-line:`Last Call`
-   :octicon:`chevron-right`
-   :bdg-primary:`Accepted`
-   :octicon:`kebab-horizontal`
-   :bdg-secondary-line:`Rejected`
-
-   :octicon:`calendar` Proposal Date: 2023-02-10
-
-   :octicon:`code-review` CL: `pwrev/128811 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/128811>`_, `pwrev/130410 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/130410>`_
+.. seed::
+   :number: 102
+   :name: Consistent Module Documentation
+   :status: Accepted
+   :proposal_date: 2023-02-10
+   :cl: 128811, 130410
 
 -------
 Summary