diff --git a/.gitignore b/.gitignore index 33a07c6741..9a1a3fc6a7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ ros2doc/ # Downloaded at HTML build time for browser-side package lists (large). source/_static/rosdistro_cache/*.yaml.gz .env + +# Downloaded at HTML build time for browser-side package lists (large). +source/_static/rosdistro_cache/*.yaml.gz diff --git a/conf.py b/conf.py index d7f1379f2e..f4987d8e92 100644 --- a/conf.py +++ b/conf.py @@ -91,39 +91,8 @@ 'sphinxcontrib.mermaid', 'ros_related_packages', 'ros_related_articles', - 'short_description', - 'pagefind_meta', - 'showmeta', ] -# pagefind search index configuration. - -pagefind_merge_enabled = False -pagefind_merge_package_pkgs = [] -pagefind_merge_index_base = 'https://docs.ros.org' -pagefind_merge_index_overrides = {} -pagefind_merge_filter_per_pkg = None -pagefind_merge_index_weight_per_pkg = None - -# Pagefind search UI (modal + /search.html): result metadata lines and facet sidebar. -# Dict keys = .. meta:: field names; values = display labels. -# Order here is facet dropdown order and result-meta line order (allowlist). -# Only listed keys are indexed as facets; keys must exist on at least one page in the build. -# Other meta (e.g. description, keywords) stays SEO-only and does not appear in the facet sidebar. - -pagefind_result_meta_order = { - 'product': 'Product', - 'distribution': 'Distribution', - 'area': 'Area', - 'capability': 'Capability', - 'community': 'Community', - 'installation': 'Installation', - 'framework': 'Framework', - 'tool': 'Tools', - 'contentType': 'Content type', - 'experience': 'Level', -} - # Intersphinx mapping intersphinx_mapping = { @@ -195,37 +164,12 @@ 'rolling': 'Rolling Ridley', } -# Tier 1 Ubuntu platform for binary deb installs (see the release page for each distro) -distro_ubuntu_deb_platform = { - 'crystal': 'Ubuntu Bionic (18.04)', - 'dashing': 'Ubuntu Bionic (18.04)', - 'eloquent': 'Ubuntu Bionic (18.04)', - 'foxy': 'Ubuntu Focal (20.04)', - 'galactic': 'Ubuntu Focal (20.04)', - 'humble': 'Ubuntu Jammy (22.04)', - 'iron': 'Ubuntu Jammy (22.04)', - 'jazzy': 'Ubuntu Noble (24.04)', - 'kilted': 'Ubuntu Noble (24.04)', - 'lyrical': 'Ubuntu Resolute Raccoon (26.04)', - 'rolling': 'Ubuntu Resolute Raccoon (26.04)', -} - -# ARM64 Ubuntu status page suffix on repo.ros2.org (ros_{distro}_{suffix}.html) -distro_arm_status_suffix = { - 'humble': 'ujv8', - 'iron': 'ujv8', - 'lyrical': 'armv8', -} - # These default values will be overridden when building multiversion macros = { 'DISTRO': 'rolling', 'DISTRO_TITLE': 'Rolling', 'DISTRO_TITLE_FULL': 'Rolling Ridley', - 'DISTRO_UBUNTU_DEB_PLATFORM': distro_ubuntu_deb_platform['rolling'], - 'DISTRO_ARM_STATUS_SUFFIX': distro_arm_status_suffix.get('rolling', 'unv8'), 'REPOS_FILE_BRANCH': 'rolling', - 'PRODUCT': 'ROS 2', } html_favicon = 'favicon.ico' @@ -239,7 +183,7 @@ html_sourcelink_suffix = '' # Relative to html_static_path -html_css_files = ['custom.css', 'adopters.css', 'pagefind-docsearch.css'] +html_css_files = ['custom.css', 'adopters.css'] html_js_files = [ ('vendor/pako.min.js', {'defer': ''}), ('vendor/js-yaml.min.js', {'defer': ''}), @@ -422,10 +366,6 @@ def smv_rewrite_configs(app, config): 'DISTRO': distro, 'DISTRO_TITLE': distro.title(), 'DISTRO_TITLE_FULL': distro_full_names[distro], - 'DISTRO_UBUNTU_DEB_PLATFORM': distro_ubuntu_deb_platform.get( - distro, 'Ubuntu Noble (24.04)' - ), - 'DISTRO_ARM_STATUS_SUFFIX': distro_arm_status_suffix.get(distro, 'unv8'), 'REPOS_FILE_BRANCH' : distro, } diff --git a/plugins/ros_related_articles.py b/plugins/ros_related_articles.py index be55a54a6e..c214be6630 100644 --- a/plugins/ros_related_articles.py +++ b/plugins/ros_related_articles.py @@ -87,20 +87,114 @@ def _normalized_value(raw: str) -> str: return ' '.join(raw.strip().lower().split()) +def _previous_sibling(node: nodes.Node) -> nodes.Node | None: + """Return the node immediately before *node* among its parent's children.""" + parent = node.parent + if parent is None: + return None + children = parent.children + idx = children.index(node) + if idx == 0: + return None + return children[idx - 1] + + +def _next_sibling(node: nodes.Node) -> nodes.Node | None: + """Return the node immediately after *node* among its parent's children.""" + parent = node.parent + if parent is None: + return None + children = parent.children + idx = children.index(node) + if idx + 1 >= len(children): + return None + return children[idx + 1] + + +def _ensure_class(node: nodes.Element, class_name: str) -> None: + """Append *class_name* to *node* if it is not already present.""" + classes = list(node.get('classes', []) or []) + if class_name not in classes: + classes.append(class_name) + node['classes'] = classes + + +def _append_article_items( + bullet_list: nodes.bullet_list, + matches: List[RelatedArticle], + app, + fromdocname: str, +) -> None: + """Append related-article links as list items to *bullet_list*.""" + for item in matches: + refuri = app.builder.get_relative_uri(fromdocname, item['docname']) + link = nodes.reference('', item['title'], refuri=refuri) + entry = nodes.list_item() + para = nodes.paragraph() + para += link + entry += para + bullet_list += entry + + +def _absorb_bullet_list( + target: nodes.bullet_list, + source: nodes.bullet_list, +) -> None: + """Move all list items from *source* onto the end of *target*.""" + for child in list(source.children): + if isinstance(child, nodes.list_item): + source.remove(child) + target.append(child) + + +def _resolve_related_articles_list( + node: RosRelatedArticlesNode, + matches: List[RelatedArticle], + app, + fromdocname: str, +) -> None: + """Replace *node* with generated links, merging adjacent manual bullet lists.""" + prev = _previous_sibling(node) + next_sib = _next_sibling(node) + prev_list = prev if isinstance(prev, nodes.bullet_list) else None + next_list = next_sib if isinstance(next_sib, nodes.bullet_list) else None + + if prev_list is not None: + target = prev_list + _ensure_class(target, 'related-articles') + else: + target = nodes.bullet_list(classes=['related-articles']) + + _append_article_items(target, matches, app, fromdocname) + + if next_list is not None: + _absorb_bullet_list(target, next_list) + next_list.replace_self([]) + + if prev_list is not None: + node.replace_self([]) + else: + node.replace_self(target) + + class RosRelatedArticlesNode(nodes.General, nodes.Element): """Placeholder node replaced during ``doctree-resolved``.""" class RosRelatedArticlesDirective(SphinxDirective): - """Emit a placeholder for static related-article links. + """Emit a placeholder replaced by a bullet list of related article links. - Uses page metadata values from ``.. meta::``: + Write the section intro (e.g. ``Related articles:``) in the RST source + before this directive. Optional bullet items immediately before or after + the directive are merged into the same list as the generated links. .. code-block:: rst .. meta:: :area: Tutorials :experience: Beginner + + Uses page metadata values from ``.. meta::`` (see above). """ has_content = False @@ -173,7 +267,7 @@ def build_related_articles_index(app, env) -> None: def resolve_related_articles(app, doctree, fromdocname) -> None: - """Replace placeholders with static paragraph + list markup.""" + """Replace placeholders with a static bullet list.""" index: List[RelatedArticle] = getattr(app.env, 'ros_related_articles_index', []) for node in list(doctree.traverse(RosRelatedArticlesNode)): area = _normalized_value(str(node.get('area', ''))) @@ -189,27 +283,19 @@ def resolve_related_articles(app, doctree, fromdocname) -> None: matches.sort(key=lambda item: item['title'].lower()) matches = matches[:max_items] + prev = _previous_sibling(node) + next_sib = _next_sibling(node) + prev_list = prev if isinstance(prev, nodes.bullet_list) else None + next_list = next_sib if isinstance(next_sib, nodes.bullet_list) else None + if not matches: + if prev_list is not None and next_list is not None: + _absorb_bullet_list(prev_list, next_list) + next_list.replace_self([]) node.replace_self([]) continue - container = nodes.container(classes=['related-articles']) - intro = nodes.paragraph() - intro += nodes.Text('Related articles:') - container += intro - - bullets = nodes.bullet_list() - for item in matches: - refuri = app.builder.get_relative_uri(fromdocname, item['docname']) - link = nodes.reference('', item['title'], refuri=refuri) - entry = nodes.list_item() - para = nodes.paragraph() - para += link - entry += para - bullets += entry - container += bullets - - node.replace_self(container) + _resolve_related_articles_list(node, matches, app, fromdocname) def setup(app): diff --git a/plugins/ros_related_packages.py b/plugins/ros_related_packages.py index 8321f18397..730299e69a 100644 --- a/plugins/ros_related_packages.py +++ b/plugins/ros_related_packages.py @@ -109,6 +109,10 @@ def _positive_int_option(argument: str) -> int: class RosRelatedPackagesDirective(SphinxDirective): """Emit a placeholder ``div`` filled at runtime by ``related_packages.js``. + Write the section intro (e.g. ``Packages/reference:``) in the RST source + before this directive. Optional bullet items immediately before or after + the directive are merged into the same list at runtime. + Filter criteria (currently ``build-type``) should be supplied as **HTML meta tags** via Docutils ``.. meta::`` so values appear in ```` and not in the page body:: diff --git a/source/How-To-Guides/Migrating-from-ROS1/Migrating-Interfaces.rst b/source/How-To-Guides/Migrating-from-ROS1/Migrating-Interfaces.rst index 9d0ffd6325..1fdcc209aa 100644 --- a/source/How-To-Guides/Migrating-from-ROS1/Migrating-Interfaces.rst +++ b/source/How-To-Guides/Migrating-from-ROS1/Migrating-Interfaces.rst @@ -1,3 +1,7 @@ +.. meta:: + :area: ROS-framework + :experience: beginner, intermediate + Migrating Interfaces ==================== @@ -59,4 +63,6 @@ This will replace ``add_message_files`` and ``add_service_files`` listing of all Related content --------------- +Related articles: + .. ros-related-articles:: \ No newline at end of file diff --git a/source/How-To-Guides/Single-Package-Define-And-Use-Interface.rst b/source/How-To-Guides/Single-Package-Define-And-Use-Interface.rst index 306da43346..1ef34bb21d 100644 --- a/source/How-To-Guides/Single-Package-Define-And-Use-Interface.rst +++ b/source/How-To-Guides/Single-Package-Define-And-Use-Interface.rst @@ -262,7 +262,13 @@ Steps Related content --------------- +Related articles: + +* some stuff here + .. ros-related-articles:: +Packages/reference: + .. ros-related-packages:: diff --git a/source/Tutorials/Beginner-Client-Libraries/Creating-A-Workspace/Creating-A-Workspace.rst b/source/Tutorials/Beginner-Client-Libraries/Creating-A-Workspace/Creating-A-Workspace.rst index 7a92e031f7..b48295a44f 100644 --- a/source/Tutorials/Beginner-Client-Libraries/Creating-A-Workspace/Creating-A-Workspace.rst +++ b/source/Tutorials/Beginner-Client-Libraries/Creating-A-Workspace/Creating-A-Workspace.rst @@ -412,4 +412,6 @@ Now that you understand the details behind creating, building and sourcing your Related content --------------- +Related articles: + .. ros-related-articles:: diff --git a/source/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.rst b/source/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.rst index 2d246cf1fc..26c96381a3 100644 --- a/source/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.rst +++ b/source/Tutorials/Beginner-Client-Libraries/Creating-Your-First-ROS2-Package.rst @@ -545,4 +545,6 @@ You'll start with a simple publisher/subscriber system, which you can choose to Related content --------------- +Related articles: + .. ros-related-articles:: diff --git a/source/_static/related_packages.js b/source/_static/related_packages.js index 8d7880e8f7..b8120e1039 100644 --- a/source/_static/related_packages.js +++ b/source/_static/related_packages.js @@ -308,9 +308,19 @@ return a.localeCompare(b); }); var picked = names.slice(0, max); + var prevList = el.previousElementSibling; + prevList = prevList && prevList.tagName === 'UL' ? prevList : null; + var nextList = el.nextElementSibling; + nextList = nextList && nextList.tagName === 'UL' ? nextList : null; + var mergeList; + + if (prevList) { + mergeList = prevList; + } else { + mergeList = document.createElement('ul'); + mergeList.className = 'related-packages__list'; + } - var ul = document.createElement('ul'); - ul.className = 'related-packages__list'; var j; for (j = 0; j < picked.length; j += 1) { var pkg = picked[j]; @@ -322,24 +332,46 @@ a.rel = 'noopener noreferrer'; li.appendChild(a); li.appendChild(document.createTextNode(': ' + description)); - ul.appendChild(li); + mergeList.appendChild(li); } - el.innerHTML = ''; el.classList.remove('related-packages--loading'); if (picked.length === 0) { + el.innerHTML = ''; + if (prevList && nextList) { + while (nextList.firstChild) { + prevList.appendChild(nextList.firstChild); + } + nextList.remove(); + prevList.classList.add('related-packages__list'); + } + if (prevList || nextList) { + el.remove(); + return; + } var p = document.createElement('p'); p.className = 'related-packages__empty'; p.textContent = 'No packages matched this filter.'; el.appendChild(p); - } else { - var intro = document.createElement('p'); - intro.className = 'related-packages__intro'; - intro.textContent = 'Packages/reference: '; - el.appendChild(intro); - el.appendChild(ul); + return; } + + if (nextList) { + while (nextList.firstChild) { + mergeList.appendChild(nextList.firstChild); + } + nextList.remove(); + } + + if (prevList) { + mergeList.classList.add('related-packages__list'); + el.remove(); + return; + } + + el.parentNode.insertBefore(mergeList, el); + el.remove(); } function fillAll() {