diff --git a/rosdoc2/verbs/build/builders/sphinx_builder.py b/rosdoc2/verbs/build/builders/sphinx_builder.py index 514bb9e..e54ea49 100644 --- a/rosdoc2/verbs/build/builders/sphinx_builder.py +++ b/rosdoc2/verbs/build/builders/sphinx_builder.py @@ -25,6 +25,7 @@ from ..create_format_map_from_package import create_format_map_from_package from ..generate_interface_docs import generate_interface_docs from ..include_user_docs import include_user_docs +from ..standard_documents import generate_standard_document_files, locate_standard_documents logger = logging.getLogger('rosdoc2') @@ -34,7 +35,10 @@ def esc_backslash(path): return path.replace('\\', '\\\\') if path else path -def generate_package_toc_entry(*, build_context, interface_counts, doc_directories) -> str: +def generate_package_toc_entry(*, build_context, + interface_counts, + doc_directories, + standard_docs) -> str: """Construct a table of content (toc) entry for the package being processed.""" build_type = build_context.build_type always_run_doxygen = build_context.always_run_doxygen @@ -47,6 +51,8 @@ def generate_package_toc_entry(*, build_context, interface_counts, doc_directori toc_entry_msg = ' Message Definitions \n' toc_entry_srv = ' Service Definitions \n' toc_entry_action = ' Action Definitions \n' + toc_entry_standard = ' Standard Documents \n' + toc_entry_readme = '.. include:: readme_include.rst' toc_doc_entry = """\ .. toctree:: :titlesonly: @@ -68,10 +74,13 @@ def generate_package_toc_entry(*, build_context, interface_counts, doc_directori if interface_counts['action'] > 0: toc_entry += toc_entry_action + if standard_docs: + toc_entry += toc_entry_standard # User documentation if doc_directories: toc_entry += toc_doc_entry - + if 'readme' in standard_docs: + toc_entry += toc_entry_readme return toc_entry @@ -467,6 +476,12 @@ def build(self, *, doc_build_folder, output_staging_directory): ) logger.info(f'interface_counts: {interface_counts}') + # locate standard documents + standard_docs = locate_standard_documents(package_xml_directory) + if standard_docs: + generate_standard_document_files(standard_docs, wrapped_sphinx_directory) + logger.info(f'standard_docs: {standard_docs}') + # include user documentation doc_directories = include_user_docs(package_xml_directory, wrapped_sphinx_directory) logger.info(f'doc_directories: {doc_directories}') @@ -519,7 +534,8 @@ def build(self, *, doc_build_folder, output_staging_directory): python_src_directory, intersphinx_mapping_extensions, interface_counts, - doc_directories) + doc_directories, + standard_docs) # If the package has python code, then invoke `sphinx-apidoc` before building has_python = self.build_context.build_type == 'ament_python' or \ @@ -642,6 +658,7 @@ def generate_wrapping_rosdoc2_sphinx_project_into_directory( intersphinx_mapping_extensions, interface_counts, doc_directories, + standard_docs, ): """Generate the rosdoc2 sphinx project configuration files.""" # Generate a default index.rst @@ -653,7 +670,8 @@ def generate_wrapping_rosdoc2_sphinx_project_into_directory( 'package_toc_entry': generate_package_toc_entry( build_context=self.build_context, interface_counts=interface_counts, - doc_directories=doc_directories) + doc_directories=doc_directories, + standard_docs=standard_docs) }) with open(os.path.join(wrapped_sphinx_directory, 'index.rst'), 'w+') as f: diff --git a/rosdoc2/verbs/build/standard_documents.py b/rosdoc2/verbs/build/standard_documents.py new file mode 100644 index 0000000..a082366 --- /dev/null +++ b/rosdoc2/verbs/build/standard_documents.py @@ -0,0 +1,112 @@ +# Copyright 2024 Open Source Robotics Foundation, Inc. +# +# 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 +# +# http://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. + +import os +import shutil + + +standard_documents_rst = """\ +Standard Documents +================== + +.. toctree:: + :maxdepth: 1 + :glob: + + standard_docs/* +""" + +readme_include_rst = """\ +README +====== + + +""" + + +def locate_standard_documents(package_xml_directory): + """Locate standard documents.""" + names = ['readme', 'license', 'contributing', 'changelog', 'quality_declaration'] + found_paths = {} + package_directory_items = os.scandir(package_xml_directory) + for item in package_directory_items: + if not item.is_file(): + continue + (basename, ext) = os.path.splitext(item.name) + for name in names: + if name in found_paths: + continue + if basename.lower() == name: + filetype = None + if ext.lower() in ['.md', '.markdown']: + filetype = 'md' + elif ext.lower() == '.rst': + filetype = 'rst' + else: + filetype = 'other' + found_paths[name] = { + 'path': item.path, + 'filename': item.name, + 'type': filetype + } + return found_paths + + +def generate_standard_document_files(standard_docs, wrapped_sphinx_directory): + """Generate rst documents to link to standard documents.""" + wrapped_sphinx_directory = os.path.abspath(wrapped_sphinx_directory) + standards_sphinx_directory = os.path.join(wrapped_sphinx_directory, 'standard_docs') + standards_original_directory = os.path.join(standards_sphinx_directory, 'original') + if len(standard_docs): + # Create the standards.rst document that will link to the actual documents + os.makedirs(standards_sphinx_directory, exist_ok=True) + os.makedirs(standards_original_directory, exist_ok=True) + standard_documents_rst_path = os.path.join( + wrapped_sphinx_directory, 'standards.rst') + with open(standard_documents_rst_path, 'w+') as f: + f.write(standard_documents_rst) + + for key, standard_doc in standard_docs.items(): + # Copy the original document to the sphinx project + shutil.copy(standard_doc['path'], standards_original_directory) + # generate the file according to type + file_contents = f'{key.upper()}\n' + # using ')' as a header marker to assure the name is the title + file_contents += ')' * len(key) + '\n\n' + file_type = standard_doc['type'] + file_path = f"original/{standard_doc['filename']}" + if file_type == 'rst': + file_contents += f'.. include:: {file_path}\n' + elif file_type == 'md': + file_contents += f'.. include:: {file_path}\n' + file_contents += ' :parser: myst_parser.sphinx_\n' + else: + file_contents += f'.. literalinclude:: {file_path}\n' + file_contents += ' :language: none\n' + with open(os.path.join(standards_sphinx_directory, f'{key.upper()}.rst'), 'w+') as f: + f.write(file_contents) + if key == 'readme': + # We create a second README to use with include + file_contents = readme_include_rst + file_path = f"standard_docs/original/{standard_doc['filename']}" + if file_type == 'rst': + file_contents += f'.. include:: {file_path}\n' + elif file_type == 'md': + file_contents += f'.. include:: {file_path}\n' + file_contents += ' :parser: myst_parser.sphinx_\n' + else: + file_contents += f'.. literalinclude:: {file_path}\n' + file_contents += ' :language: none\n' + with open(os.path.join(wrapped_sphinx_directory, 'readme_include.rst'), 'w+') as f: + f.write(file_contents) diff --git a/test/packages/full_package/doc/somethingElse.markdown b/test/packages/full_package/doc/somethingElse.markdown new file mode 100644 index 0000000..5854886 --- /dev/null +++ b/test/packages/full_package/doc/somethingElse.markdown @@ -0,0 +1,2 @@ +# Alternate Markdown type +.markdown should also be recognized as markdown. diff --git a/test/packages/full_package/quality_declaration.markdown b/test/packages/full_package/quality_declaration.markdown new file mode 100644 index 0000000..65b7de0 --- /dev/null +++ b/test/packages/full_package/quality_declaration.markdown @@ -0,0 +1 @@ +This document is a declaration of software quality based on the guidelines in [REP-2004](https://www.ros.org/reps/rep-2004.html). diff --git a/test/packages/only_messages/README.md b/test/packages/only_messages/README.md deleted file mode 100644 index d7e826b..0000000 --- a/test/packages/only_messages/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# This is the readme - -This file is included to make sure that the standard documents appear when there is no other documentation. diff --git a/test/packages/only_messages/README.rst b/test/packages/only_messages/README.rst new file mode 100644 index 0000000..641b8bb --- /dev/null +++ b/test/packages/only_messages/README.rst @@ -0,0 +1,5 @@ +This is the readme +================== + +This file is included to make sure that the standard documents appear when there is +no other documentation. Also, this is an rst readme. diff --git a/test/packages/only_python/README.txt b/test/packages/only_python/README.txt new file mode 100644 index 0000000..c405d7f --- /dev/null +++ b/test/packages/only_python/README.txt @@ -0,0 +1 @@ +This is a README as simple text. diff --git a/test/test_builder.py b/test/test_builder.py index 7ca89bc..2c8cec9 100644 --- a/test/test_builder.py +++ b/test/test_builder.py @@ -84,11 +84,12 @@ def do_test_package( file_includes=[], file_excludes=[], links_exist=[], + fragments=[], ) -> None: """Test that package documentation exists and includes/excludes certain text. :param pathlib.Path work_path: path where generated files were placed - :param list[str] includes: lower case text found in index.html data + :param list[str] includes: lower case text found exactly in index.html data :param list[str] excludes: lower case text not found in index.html data :param list[str] file_includes: path to files (relative to root index.html directory) of files that should exist @@ -96,6 +97,7 @@ def do_test_package( (relative to root index.html directory) of files that should not exist :param list[str] links_exist: Confirm that 1) a link exists containing this text, and 2) the link is a valid file + :param list[str] fragments: lower case text found partially in index.html data """ logger.info(f'*** Testing package {name} work_path {work_path}') output_dir = work_path / 'output' @@ -157,6 +159,16 @@ def do_test_package( assert link_path.is_file(), \ f'file represented by <{found_item}> should exist at <{link_path}>' + # look for fragments of text + for item in fragments: + found_fragment = False + for text in parser.content: + if item in text: + found_fragment = True + break + assert found_fragment, \ + f'html should have text fragment <{item}>' + def test_minimum_package(session_dir): """Tests of a package containing as little as possible.""" @@ -199,6 +211,7 @@ def test_full_package(session_dir): 'service definitions', 'action definitions', 'instructions', # has documentation + 'changelog', ] file_includes = [ 'generated/index.html' @@ -206,16 +219,21 @@ def test_full_package(session_dir): links_exist = [ 'full_package.dummy.html', 'modules.html', - 'user_docs/morestuff/more_of_more/subsub.html' # a deep documentation file + 'user_docs/morestuff/more_of_more/subsub.html', # a deep documentation file + 'standards.html', ] excludes = [ 'dontshowme' ] + fragments = [ + 'this is the package readme.', + ] do_test_package(PKG_NAME, session_dir, includes=includes, file_includes=file_includes, excludes=excludes, - links_exist=links_exist) + links_exist=links_exist, + fragments=fragments) def test_only_python(session_dir):