From 5a249726b34d82d60cded409e25215fb4e07197b Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Wed, 29 Apr 2026 18:43:01 +0200 Subject: [PATCH] Fix #238: Add user portal schema doc * Describe the portal schema from the perspective of a user trying to add or change the config. * Amend the glossary with more terms or redefine some terms * Config changes: * Use css/custom.css instead of css/style.css * Use SUSE font * Apply styling for glossary * Add glossary processing to conf.py to make letters appear in the body and on the right side toc. --- .../_static/css/{style.css => custom.css} | 9 +- docs/source/conf.py | 139 +++++++ docs/source/glossary.rst | 93 ++++- docs/source/user/config/index.rst | 6 +- docs/source/user/index.rst | 1 + .../user/portal-config/add-deliverable.rst | 78 ++++ .../user/portal-config/add-translations.rst | 53 +++ .../user/portal-config/building-blocks.rst | 364 ++++++++++++++++++ .../user/portal-config/change-lifecycle.rst | 35 ++ .../user/portal-config/create-links.rst | 49 +++ .../user/portal-config/create-portal.rst | 39 ++ .../user/portal-config/create-product.rst | 103 +++++ .../user/portal-config/create-xrefs.rst | 30 ++ .../user/portal-config/group-deliverables.rst | 34 ++ docs/source/user/portal-config/index.rst | 23 ++ .../user/portal-config/mark-spotlight.rst | 35 ++ 16 files changed, 1075 insertions(+), 16 deletions(-) rename docs/source/_static/css/{style.css => custom.css} (50%) create mode 100644 docs/source/user/portal-config/add-deliverable.rst create mode 100644 docs/source/user/portal-config/add-translations.rst create mode 100644 docs/source/user/portal-config/building-blocks.rst create mode 100644 docs/source/user/portal-config/change-lifecycle.rst create mode 100644 docs/source/user/portal-config/create-links.rst create mode 100644 docs/source/user/portal-config/create-portal.rst create mode 100644 docs/source/user/portal-config/create-product.rst create mode 100644 docs/source/user/portal-config/create-xrefs.rst create mode 100644 docs/source/user/portal-config/group-deliverables.rst create mode 100644 docs/source/user/portal-config/index.rst create mode 100644 docs/source/user/portal-config/mark-spotlight.rst diff --git a/docs/source/_static/css/style.css b/docs/source/_static/css/custom.css similarity index 50% rename from docs/source/_static/css/style.css rename to docs/source/_static/css/custom.css index ac179964..ae69479f 100644 --- a/docs/source/_static/css/style.css +++ b/docs/source/_static/css/custom.css @@ -5,4 +5,11 @@ html { --pst-font-family-base: SUSE, var(--pst-font-family-base-system); --pst-font-family-heading: SUSE, sans-serif, var(--pst-font-family-base-system); --pst-font-family-monospace: Courier, var(--pst-font-family-monospace-system); -} \ No newline at end of file +} + +/* Add a horizontal line below the glossary letter titles */ +.glossary-letter-title { + border-bottom: 1px solid var(--pst-color-border); /* Use theme's border color */ + padding-bottom: 0.5em; /* Space between title text and the line */ + margin-bottom: 1em; /* Space between the line and the content below */ +} diff --git a/docs/source/conf.py b/docs/source/conf.py index f6de5a5f..cabd4c4d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,9 +9,12 @@ from datetime import datetime from pathlib import Path import sys +from typing import Self +from docutils import nodes from sphinx.application import Sphinx from sphinx.errors import ExtensionError +from sphinx.transforms import SphinxTransform from sphinx.util import logging from docbuild.__about__ import __version__ @@ -287,7 +290,143 @@ def run_toml_generator(app: Sphinx) -> None: generate_toml_reference(toml_input, rst_output, **config_data) +class GlossaryRearranger(SphinxTransform): + """Group glossary terms by first letter into section nodes. + + The glossary directive emits a flat definition list. This transform rewrites + it into per-letter sections so each letter gets its own heading and anchor. + """ + + default_priority = 400 + + @staticmethod + def _is_glossary_definition_list(node: nodes.definition_list) -> bool: + """Return whether a definition list belongs to a glossary block.""" + parent = node.parent + if parent is None: + return False + + return "glossary" in parent.get("classes", []) or "glossary" in node.get( + "classes", [] + ) + + @staticmethod + def _group_terms_by_letter( + node: nodes.definition_list, + ) -> dict[str, list[nodes.definition_list_item]]: + """Group glossary items by the first letter of each term.""" + groups: dict[str, list[nodes.definition_list_item]] = {} + for item in list(node.children): + if not isinstance(item, nodes.definition_list_item): + continue + + term_node = item.next_node(nodes.term) + if not term_node: + continue + + term_text = term_node.astext().strip() + if not term_text: + continue + + letter = term_text[0].upper() + groups.setdefault(letter, []).append(item) + + return groups + + @staticmethod + def _build_letter_sections( + groups: dict[str, list[nodes.definition_list_item]], + ) -> list[nodes.section]: + """Build one section per glossary letter group.""" + new_sections: list[nodes.section] = [] + for letter in sorted(groups.keys()): + sec = nodes.section( + ids=[letter], + classes=["glossary-section", f"glossary-letter-{letter.lower()}"], + ) + sec += nodes.title(letter, letter, classes=["glossary-letter-title"]) + + letter_list = nodes.definition_list() + letter_list.extend(groups[letter]) + sec += letter_list + new_sections.append(sec) + + return new_sections + + @staticmethod + def _replace_with_sections( + parent: nodes.Element, + node: nodes.definition_list, + new_sections: list[nodes.section], + ) -> None: + """Replace glossary list with generated sections in the proper parent.""" + if getattr(parent, "tagname", "") == "glossary" and parent.parent is not None: + # Sphinx's ToC collector ignores
nodes nested inside a + # node. Hoist sections one level up so they are indexed. + grandparent = parent.parent + idx = grandparent.index(parent) + for sec in reversed(new_sections): + grandparent.insert(idx, sec) + parent.remove(node) + if not parent.children: + grandparent.remove(parent) + return + + node.replace_self(new_sections) + + def apply(self: Self) -> None: + """Rewrite glossary definition lists into grouped letter sections.""" + for node in list(self.document.findall(nodes.definition_list)): + if node.get("_glossary_rearranged", False): + continue + + if not self._is_glossary_definition_list(node): + continue + + groups = self._group_terms_by_letter(node) + if not groups: + continue + + new_sections = self._build_letter_sections(groups) + parent = node.parent + if not isinstance(parent, nodes.Element): + continue + + self._replace_with_sections(parent, node, new_sections) + + node["_glossary_rearranged"] = True + logger.info("GlossaryRearranger: Grouped %d letters.", len(groups)) + + +class GlossaryToCBuilder(SphinxTransform): + """Add glossary term permalinks used by the rendered HTML output. + + This transform keeps generated glossary terms linkable via header links. + """ + + default_priority = 950 + + def apply(self) -> None: + """Attach permalink references to glossary term nodes.""" + # Inject Term Permalinks (existing logic) + for term_node in self.document.findall(nodes.term): + t_ids = term_node.get("ids", []) + if t_ids and not any( + isinstance(c, nodes.reference) for c in term_node.children + ): + t_ref = nodes.reference( + "", + "#", + refuri=f"#{t_ids[0]}", + classes=["headerlink"], + reftitle="Permalink", + ) + term_node += t_ref + + def setup(app: Sphinx) -> None: """Sphinx setup function to connect the TOML generator to the build process.""" # This hook ensures the file exists before Sphinx tries to 'include' it app.connect("builder-inited", run_toml_generator) + app.add_transform(GlossaryRearranger) + app.add_transform(GlossaryToCBuilder) diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 3a87d78f..263e78f3 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -1,8 +1,6 @@ Glossary ======== -:term:`A ` | :term:`C ` | :term:`D ` | :term:`G ` | :term:`I ` | :term:`L ` | :term:`M ` | :term:`O