diff --git a/.github/workflows/doc_checks.yml b/.github/workflows/doc_checks.yml index 4a6280cb3d5b..eee389f10313 100644 --- a/.github/workflows/doc_checks.yml +++ b/.github/workflows/doc_checks.yml @@ -81,11 +81,6 @@ jobs: run: | mkdir -p doc/build doxygen Doxyfile - - name: Generated RST files - shell: bash -l {0} - run: | - make generated_rst_files - working-directory: ./doc - name: Spelling shell: bash -l {0} run: | diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 58b2285412a8..cf89aec5477d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -15,7 +15,7 @@ build: - (git --no-pager log --pretty="tformat:%s -- %b" -1 | paste -s -d " " | grep -viqP "skip ci|ci skip") || exit 183 pre_build: - ./doc/rtd/pre_build.sh - - cd doc && make doxygen generated_rst_files + - cd doc && make doxygen apt_packages: - ant diff --git a/doc/Makefile b/doc/Makefile index be1b5e0e353a..ba8426fcf24d 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -14,7 +14,7 @@ BUILDDIR = build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile clean generated_rst_files doxygen +.PHONY: help Makefile clean doxygen # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). @@ -67,22 +67,19 @@ $(BUILDDIR)/.doxygen_up_to_date: for i in $(BUILDDIR)/xml/*.xml; do sed "s//--/g" < $$i > $$i.tmp; mv $$i.tmp $$i; done; \ touch $(BUILDDIR)/.doxygen_up_to_date -generated_rst_files: $(BUILDDIR)/.doxygen_up_to_date - $(PYTHON) "$(SOURCEDIR)/build_driver_summary.py" "$(SOURCEDIR)/drivers/raster" raster_driver_summary "$(SOURCEDIR)/drivers/raster/driver_summary.rst" - $(PYTHON) "$(SOURCEDIR)/build_driver_summary.py" "$(SOURCEDIR)/drivers/vector" vector_driver_summary "$(SOURCEDIR)/drivers/vector/driver_summary.rst" .PHONY: html latexpdf -html: generated_rst_files +html: $(BUILDDIR)/.doxygen_up_to_date BUILDDIR="${BUILDDIR}" $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ln -sf ../latex/gdal.pdf $(BUILDDIR)/html -man: generated_rst_files +man: BUILDDIR="${BUILDDIR}" $(SPHINXBUILD) -M man "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -latexpdf: generated_rst_files +latexpdf: BUILDDIR="${BUILDDIR}" $(SPHINXBUILD) -M latexpdf "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -spelling: generated_rst_files +spelling: $(BUILDDIR)/.doxygen_up_to_date BUILDDIR="${BUILDDIR}" $(SPHINXBUILD) -b spelling "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) clean: diff --git a/doc/source/_extensions/driverproperties.py b/doc/source/_extensions/driverproperties.py index f75611662a56..679ccdf83c8e 100644 --- a/doc/source/_extensions/driverproperties.py +++ b/doc/source/_extensions/driverproperties.py @@ -2,6 +2,10 @@ import docutils.statemachine import sphinx.locale +from docutils import nodes +from sphinx.locale import _ +from sphinx.util import logging +from sphinx.util.docutils import SphinxDirective sphinx.locale.admonitionlabels["shortname"] = "" sphinx.locale.admonitionlabels["built_in_by_default"] = "" # 'Built-in by default' @@ -93,11 +97,13 @@ def setup(app): app.add_directive("deprecated_driver", DeprecatedDriverDirective) app.connect("env-purge-doc", purge_driverproperties) + app.connect("env-merge-info", merge_driverproperties) - return {"parallel_read_safe": True, "parallel_write_safe": True} - + app.add_node(driver_index) + app.add_directive("driver_index", DriverIndex) + app.connect("doctree-resolved", create_driver_index) -from docutils import nodes + return {"parallel_read_safe": True, "parallel_write_safe": True} def visit_admonition_generic(self, node): @@ -200,10 +206,6 @@ def visit_deprecated_driver_node_html(self, node): self.body.append(self.starttag(node, "div", CLASS=("danger deprecated_driver"))) -from docutils.parsers.rst import Directive -from sphinx.locale import _ - - def finish_directive(_self, directive, node): env = _self.state.document.settings.env @@ -227,7 +229,66 @@ def finish_directive(_self, directive, node): return [targetnode, node] -class ShortName(Directive): +class DriverPropertyDirective(SphinxDirective): + def driver_properties(self): + + if not hasattr(self.env, "gdal_drivers"): + self.env.gdal_drivers = {} + + # Search for a .. shortname:: directive in the current document + try: + short_name_node = next(self.env.parser.document.findall(shortname)) + short_name = short_name_node.children[1].astext() + + except StopIteration: + if "plscenes" in self.env.docname: + return {} + + logger = logging.getLogger(__name__) + logger.warning( + "Driver does not have a 'shortname' directive.", + location=self.env.docname, + ) + + return {} + + if short_name not in self.env.gdal_drivers: + # Initialize driver properties object + try: + long_name_node = next(self.env.parser.document.findall(nodes.title)) + except StopIteration: + logger = logging.getLogger(__name__) + logger.warning( + "Driver document does not have a title.", + location=self.env.docname, + ) + + return {} + + long_name = long_name_node.astext() + if " -- " in long_name: + long_name = long_name.split(" -- ")[1].strip() + if " - " in long_name and "OGC API" not in long_name: + long_name = long_name.split(" - ")[1].strip() + + self.env.gdal_drivers[short_name] = { + "docname": self.env.docname, + "short_name": short_name, + "long_name": long_name, + "built_in_by_default": False, + "supports_create": False, + "supports_createcopy": False, + "supports_georeferencing": False, + "supports_virtualio": False, + "supports_multidimensional": False, + "deprecated": False, + "requirements": "", + } + + return self.env.gdal_drivers[short_name] + + +class ShortName(SphinxDirective): # this enables content in the directive has_content = True @@ -240,12 +301,13 @@ def run(self): return finish_directive(self, "shortname", node) -class BuiltInByDefault(Directive): +class BuiltInByDefault(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["built_in_by_default"] = True if not self.content: self.content = docutils.statemachine.StringList( @@ -259,12 +321,13 @@ def run(self): return finish_directive(self, "built_in_by_default", node) -class BuildDependencies(Directive): +class BuildDependencies(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["requirements"] = ", ".join(self.content) assert ( self.content @@ -275,12 +338,13 @@ def run(self): return finish_directive(self, "build_dependencies", node) -class CreateDirective(Directive): +class CreateDirective(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["supports_create"] = True if not self.content: self.content = docutils.statemachine.StringList( @@ -292,12 +356,13 @@ def run(self): return finish_directive(self, "supports_create", node) -class CreateCopyDirective(Directive): +class CreateCopyDirective(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["supports_createcopy"] = True if not self.content: self.content = docutils.statemachine.StringList( @@ -311,12 +376,13 @@ def run(self): return finish_directive(self, "supports_createcopy", node) -class GeoreferencingDirective(Directive): +class GeoreferencingDirective(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["supports_georeferencing"] = True if not self.content: self.content = docutils.statemachine.StringList( @@ -328,12 +394,13 @@ def run(self): return finish_directive(self, "supports_georeferencing", node) -class VirtualIODirective(Directive): +class VirtualIODirective(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["supports_virtualio"] = True if not self.content: self.content = docutils.statemachine.StringList( @@ -347,12 +414,13 @@ def run(self): return finish_directive(self, "supports_virtualio", node) -class MultiDimensionalDirective(Directive): +class MultiDimensionalDirective(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["supports_multidimensional"] = True if not self.content: self.content = docutils.statemachine.StringList( @@ -366,12 +434,13 @@ def run(self): return finish_directive(self, "supports_multidimensional", node) -class DeprecatedDriverDirective(Directive): +class DeprecatedDriverDirective(DriverPropertyDirective): # this enables content in the directive has_content = True def run(self): + self.driver_properties()["deprecated"] = True explanation = [] version_targeted_for_removal = [ @@ -427,7 +496,135 @@ def run(self): return finish_directive(self, "deprecated_driver", node) +### Driver Index + + +class driver_index(nodes.General, nodes.Element): + pass + + +class DriverIndex(SphinxDirective): + + has_content = True + required_arguments = 0 + option_spec = {"type": str} + + def run(self): + index_placeholder = driver_index("") + + if "type" in self.options: + index_placeholder["driver_type"] = self.options["type"] + + return [index_placeholder] + + +def create_driver_index(app, doctree, fromdocname): + # This event handler is called on "doctree-resolved" + # It replaces the driver_index placeholder(s) with + # tables of drivers. + + env = app.builder.env + + if not hasattr(env, "gdal_drivers"): + env.gdal_drivers = {} + + for node in doctree.findall(driver_index): + + if "driver_type" in node and node["driver_type"] == "vector": + columns = { + "short_name": {"name": "Short Name", "width": 10}, + "long_name": {"name": "Long Name", "width": 20}, + "supports_create": {"name": "Creation", "width": 10}, + "supports_georeferencing": {"name": "Georeferencing", "width": 10}, + "requirements": {"name": "Build Requirements", "width": 20}, + } + else: + columns = { + "short_name": {"name": "Short Name", "width": 10}, + "long_name": {"name": "Long Name", "width": 35}, + "supports_create": {"name": "Creation (1)", "width": 10}, + "supports_createcopy": {"name": "Copy (2)", "width": 10}, + "supports_georeferencing": {"name": "Georeferencing", "width": 10}, + "requirements": {"name": "Build Requirements", "width": 25}, + } + + tgroup = nodes.tgroup(cols=len(columns)) + + for col in columns: + colspec = nodes.colspec(colwidth=columns[col]["width"]) + tgroup.append(colspec) + + header = nodes.row() + for col in columns: + header.append(nodes.entry("", nodes.paragraph(text=columns[col]["name"]))) + tgroup.append(nodes.thead("", header)) + + tbody = nodes.tbody() + + for short_name in sorted(env.gdal_drivers, key=str.casefold): + driver_properties = env.gdal_drivers[short_name] + + if ( + "driver_type" in node + and node["driver_type"] not in driver_properties["docname"] + ): + continue + + row = nodes.row() + + for col in columns: + cell = nodes.entry() + row.append(cell) + + if col == "short_name": + ref = nodes.reference( + refuri=app.builder.get_relative_uri( + fromdocname, driver_properties["docname"] + ), + internal=True, + ) + ref.append(nodes.Text(short_name)) + para = nodes.paragraph() + para.append(ref) + cell.append(para) + else: + value = driver_properties[col] + + if col == "requirements" and not value: + if driver_properties["built_in_by_default"]: + value = "Built-in by default" + + if type(value) is bool: + if value: + cell.append(nodes.strong("Yes", "Yes")) + else: + cell.append(nodes.Text("No")) + else: + cell.append(nodes.Text(value)) + + tbody.append(row) + + tgroup.append(tbody) + + table = nodes.table("", tgroup) + node.replace_self(table) + + +def merge_driverproperties(app, env, docnames, other): + if not hasattr(env, "gdal_drivers"): + env.gdal_drivers = {} + + if hasattr(other, "gdal_drivers"): + for k, v in other.gdal_drivers.items(): + env.gdal_drivers[k] = v + + def purge_driverproperties(app, env, docname): + if hasattr(env, "gdal_drivers"): + env.gdal_drivers = { + k: v for k, v in env.gdal_drivers.items() if v["docname"] != docname + } + for directive in [ "all_shortname", "all_built_in_by_default", diff --git a/doc/source/build_driver_summary.py b/doc/source/build_driver_summary.py deleted file mode 100755 index ff1bd90c822f..000000000000 --- a/doc/source/build_driver_summary.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 - -import glob -import os -import sys - -dirname = sys.argv[1] -anchor = sys.argv[2] -outfile = sys.argv[3] - -res = [] - -excluded_filenames = ["driver_summary", "wms_wmts_cache"] - -for filename in glob.glob(os.path.join(dirname, "*.rst")): - if os.path.basename(filename)[0:-4] in excluded_filenames: - continue - with open(filename, "rt", encoding="utf-8") as f: - shortnames = [] - longname = None - supports_create = False - supports_createcopy = False - supports_georeferencing = False - supports_virtualio = False - built_in_by_default = False - build_dependencies = None - link = None - last_line = None - for l in f.readlines(): - l = l.rstrip("\n") - if not link: - assert l.startswith(".. _") and l.endswith(":"), (filename, l) - link = l[len(".. _") : -1] - elif l.startswith(".. shortname:: "): - shortname = l[len(".. shortname:: ") :] - if shortname not in ("HDF4Image", "HDF5Image"): - shortnames.append(shortname) - elif ( - longname is None - and last_line != "" - and l.startswith("=" * len(last_line)) - ): - longname = last_line - pos = longname.find(" -- ") - if pos > 0: - longname = longname[pos + len(" -- ") :] - pos = longname.find(" - ") - if pos > 0 and "oapif" not in link: - longname = longname[pos + len(" - ") :] - line_before_long_name = False - elif l.startswith(".. supports_createcopy::"): - supports_createcopy = True - elif l.startswith(".. supports_create::"): - supports_create = True - elif l.startswith(".. supports_georeferencing::"): - supports_georeferencing = True - elif l.startswith(".. supports_virtualio::"): - supports_virtualio = True - elif l.startswith(".. built_in_by_default::"): - built_in_by_default = True - elif l.startswith(".. build_dependencies:: "): - build_dependencies = l[len(".. build_dependencies:: ") :] - last_line = l - for shortname in shortnames: - res.append( - [ - link, - shortname, - longname, - built_in_by_default, - build_dependencies, - supports_create, - supports_createcopy, - supports_georeferencing, - supports_virtualio, - ] - ) - -res = sorted(res, key=lambda row: row[0].lower()) - -with open(outfile, "wt", encoding="utf-8") as f: - f.write(".. %s:\n\n" % anchor) - f.write("..\n") - f.write(" This file is generated by build_driver_summary.py. DO NOT EDIT !!!\n") - f.write(" Do not put in git !!!\n") - f.write("..\n") - f.write(".. list-table::\n") - if anchor == "raster_driver_summary": - f.write(" :widths: 10 35 10 10 10 25\n") - else: - f.write(" :widths: 10 20 10 10 20\n") - f.write(" :header-rows: 1\n") - f.write("\n") - f.write(" * - Short name\n") - f.write(" - Long name\n") - if anchor == "raster_driver_summary": - f.write(" - Creation (1)\n") - f.write(" - Copy (2)\n") - else: - f.write(" - Creation\n") - f.write(" - Geo-referencing\n") - # f.write(" - Virtual I/O\n") - f.write(" - Build requirements\n") - for ( - link, - shortname, - longname, - built_in_by_default, - build_dependencies, - supports_create, - supports_createcopy, - supports_georeferencing, - supports_virtualio, - ) in res: - f.write(" * - :ref:`%s <%s>`\n" % (shortname, link)) - f.write(" - %s\n" % longname) - f.write(" - %s\n" % ("**Yes**" if supports_create else "No")) - if anchor == "raster_driver_summary": - f.write(" - %s\n" % ("**Yes**" if supports_createcopy else "No")) - f.write(" - %s\n" % ("**Yes**" if supports_georeferencing else "No")) - # f.write(" - %s\n" % ('**Yes**' if supports_virtualio else 'No')) - if built_in_by_default: - f.write(" - %s\n" % "Built-in by default") - elif build_dependencies: - f.write(" - %s\n" % build_dependencies) - else: - f.write(" - %s\n" % "???") - f.write("\n") - if anchor == "raster_driver_summary": - f.write("- (1): Creation refers to implementing :cpp:func:`GDALCreate`.\n") - f.write("- (2): Copy refers to implementing :cpp:func:`GDALCreateCopy`.\n") diff --git a/doc/source/drivers/raster/index.rst b/doc/source/drivers/raster/index.rst index e22b67ce333a..b732c036ccb6 100644 --- a/doc/source/drivers/raster/index.rst +++ b/doc/source/drivers/raster/index.rst @@ -4,13 +4,13 @@ Raster drivers ================================================================================ -.. - Note to contributors. The array-style presentation of drivers comes from - driver_summary.rst, which is file generated by - https://github.com/OSGeo/gdal/blob/master/doc/source/build_driver_summary.py - during the build. It is *not* stored in git. +.. raster_driver_summary: -.. include:: driver_summary.rst +.. driver_index:: + :type: raster + +- (1): Creation refers to implementing :cpp:func:`GDALCreate`. +- (2): Copy refers to implementing :cpp:func:`GDALCreateCopy`. .. note:: diff --git a/doc/source/drivers/vector/index.rst b/doc/source/drivers/vector/index.rst index 7f5fa2b2a081..39e09c8e26dd 100644 --- a/doc/source/drivers/vector/index.rst +++ b/doc/source/drivers/vector/index.rst @@ -4,14 +4,10 @@ Vector drivers ================================================================================ -.. - Note to contributors. The array-style presentation of drivers comes from - driver_summary.rst, which is file generated by - https://github.com/OSGeo/gdal/blob/master/doc/source/build_driver_summary.py - during the build. It is *not* stored in git. - -.. include:: driver_summary.rst +.. vector_driver_summary: +.. driver_index:: + :type: vector .. note::