summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimo Röhling <roehling@debian.org>2023-11-19 00:55:40 +0100
committerTimo Röhling <roehling@debian.org>2023-11-19 00:55:40 +0100
commitb20a4d73e8176f0bfcd40ed36842d4d3aeb51748 (patch)
treec0b0b1f5ea8c1be6526536971555bd48c935022c
New upstream version 3.27.0
-rw-r--r--.github/dependabot.yml7
-rw-r--r--.github/workflows/cd.yml43
-rw-r--r--.github/workflows/ci.yml31
-rw-r--r--.gitignore163
-rw-r--r--.pre-commit-config.yaml20
-rw-r--r--LICENSE34
-rw-r--r--README.md116
-rw-r--r--noxfile.py65
-rw-r--r--pyproject.toml60
-rw-r--r--sphinxcontrib/moderncmakedomain/__init__.py3
-rw-r--r--sphinxcontrib/moderncmakedomain/cmake.py748
-rw-r--r--sphinxcontrib/moderncmakedomain/colors.py29
-rw-r--r--tests/conftest.py9
-rw-r--r--tests/roots/test-root/conf.py1
-rw-r--r--tests/roots/test-root/external.rst5
-rw-r--r--tests/roots/test-root/index.rst7
-rw-r--r--tests/roots/test-root/local.rst17
-rw-r--r--tests/roots/test-root/more.rst5
-rw-r--r--tests/roots/test-root/padding.rst4
-rw-r--r--tests/roots/test-root/parallel.rst16
-rw-r--r--tests/test_basic.py27
-rw-r--r--tests/test_version.py7
22 files changed, 1417 insertions, 0 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..2c7d170
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 0000000..1dabd41
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,43 @@
+name: CD
+
+on:
+ workflow_dispatch:
+ release:
+ types:
+ - published
+
+
+jobs:
+ dist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Build SDist and wheel
+ run: pipx run build
+
+ - uses: actions/upload-artifact@v3
+ with:
+ path: dist/*
+
+ - name: Check metadata
+ run: pipx run twine check dist/*
+
+
+ publish:
+ needs: [dist]
+ runs-on: ubuntu-latest
+ if: github.event_name == 'release' && github.event.action == 'published'
+ environment:
+ name: pypi
+ url: https://pypi.org/p/sphinxcontrib-moderncmakedomain
+ permissions:
+ id-token: write
+
+ steps:
+ - uses: actions/download-artifact@v3
+ with:
+ name: artifact
+ path: dist
+
+ - uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b0aeaf9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+name: CI
+
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+
+env:
+ FORCE_COLOR: 3
+
+jobs:
+ check-package:
+ name: Build & inspect
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: hynek/build-and-inspect-python-package@v1
+
+ test:
+ name: Run quick tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: wntrblm/nox@2023.04.22
+ with:
+ python-versions: "3.7, 3.8, 3.9, 3.10, 3.11, 3.12-dev"
+ - run: nox -s tests
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c513a42
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,163 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# OS-specific
+.DS_Store
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..a10bd4e
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,20 @@
+ci:
+ autoupdate_commit_msg: "chore: update pre-commit hooks"
+ autofix_commit_msg: "style: pre-commit fixes"
+
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: "v4.3.0"
+ hooks:
+ - id: check-added-large-files
+ - id: check-case-conflict
+ - id: check-merge-conflict
+ - id: check-symlinks
+ - id: check-yaml
+ - id: debug-statements
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ - id: name-tests-test
+ args: ["--pytest-test-first"]
+ - id: requirements-txt-fixer
+ - id: trailing-whitespace
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2302413
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,34 @@
+CMake - Cross Platform Makefile Generator
+Copyright 2000-2018 Kitware, Inc. and Contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Kitware, Inc. nor the names of Contributors
+ may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+---
+
+See https://cmake.org/licensing for more details
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e4b4193
--- /dev/null
+++ b/README.md
@@ -0,0 +1,116 @@
+# Sphinx Domain for Modern CMake
+
+This is taken directly from the Kitware git repository's Utilities directory.
+The original [sphinxcontrib-cmakedomain][] has not been touched in quite some and
+as a result it was wildly out of date. Documenting CMake domain entities in
+projects is painful otherwise. This works *exactly* in the same way as Kitware,
+so some time might be needed to study their approach to these problems.
+
+This repository is under the same License as all of CMake, which is the
+BSD-3-Clause license.
+
+🚨🚨🚨
+Any issues you run into with this plugin must be reported to [Kitware][],
+unless they involve the packaging itself. The Python files exactly match
+the CMake source for the released version numbers.
+🚨🚨🚨
+
+# Installation
+
+## PyPI
+
+This domain is available via PyPI. Install it directly via `pip`:
+
+```
+$ pip install sphinxcontrib-moderncmakedomain
+```
+
+Alternatively, place it inside of your `setup.py`, `pyproject.toml`,
+`requirements.txt` or whatever system it is that you use to declare and manage
+your dependencies. A new version will usually only be released if there is a
+change to this extension inside CMake.
+
+## Git
+
+This module is installable via `pip` and GitHub directly as well
+
+```
+$ pip install git+https://github.com/scikit-build/moderncmakedomain.git
+```
+
+# Usage
+
+To enable the use of the `moderncmakedomain`, add
+`sphinxcontrib.moderncmakedomain` to the `extensions` variable of your
+`conf.py` file:
+
+```python
+extensions = [..., 'sphinxcontrib.moderncmakedomain', ...]
+```
+
+The plugin currently provides several directives and references. These are
+documented below.
+
+## Directives
+
+| directive | description |
+|:------------------:|:----------------------------------------------------|
+| `cmake:variable::` | For a basic variable |
+| `cmake:command::` | For a function |
+| `cmake-module::` | Autodoc style extractor (takes a relative filepath) |
+| `cmake:envvar::` | For environment variables |
+
+To declare any of the references found below, they must be placed into a
+directory with the same name under the sphinx SOURCEDIR/master doc. Thus,
+`prop_tgt/MY_PERSONAL_PROPERTY.rst` can be referred to with
+``:prop_tgt:`MY_PERSONAL_PROPERTY` ``. This is currently the *only* way CMake
+permits declaring new properties.
+
+## References
+
+Each reference below can be placed into a directory with the same name to
+document custom extensions provided by your CMake libraries.
+
+| ref | description |
+|:--------------:|:---------------------------------------------------|
+| `:variable:` | Refer to a CMake variable |
+| `:command:` | Refer to a CMake command |
+| `:envvar:` | Refers to an environment variable |
+| `:cpack_gen:` | Refers to CPack generators |
+| `:generator:` | Refers to a build file generator |
+| `:genex:` | Refers to a generator expression |
+| `:guide:` | Used to refer to a "guide" page |
+| `:manual:` | Used to refer to a "manual" page (like `cmake(1)`) |
+| `:policy:` | Refers to CMake Policies |
+| `:module:` | Refers to CMake Modules |
+| `:prop_tgt:` | For target properties |
+| `:prop_test:` | For test properties |
+| `:prop_sf:` | For source file properties |
+| `:prop_gbl:` | For global properties |
+| `:prop_dir:` | For directory properties |
+| `:prop_inst:` | For installed file properties |
+| `:prop_cache:` | For cache properties |
+
+# History
+
+`sphinx-moderncmakedomain` was initially developed in October 2018 by
+[slurps-mad-rips][slurps-mad-rips] to help write CMake documentation by simply
+publishing a python package of the same. This was a critical step to ease the
+maintenance of sphinx-based documentation and avoid systematically copying the
+associated python module maintained within the CMake repository.
+
+Later in early August 2021, [henryiii][henryiii] discovered the
+`sphinx-moderncmakedomain` project while working on scikit-build issue
+[#574][skbuild-issue-574] intended to simplify its documentation generation
+infrastructure and avoid updating its own copy of the sphinx extension.
+[henryiii][henryiii] and [jcfr][jcfr] then worked with
+[slurps-mad-rips][slurps-mad-rips] to establish a transition plan to
+collaboratively maintain the project within the scikit-build organization.
+
+[sphinxcontrib-cmakedomain]: https://github.com/sphinx-contrib/cmakedomain
+[Kitware]: https://gitlab.kitware.com/
+
+[skbuild-issue-574]: https://github.com/scikit-build/scikit-build/pull/574
+[slurps-mad-rips]: https://github.com/slurps-mad-rips
+[henryiii]: https://github.com/henryiii
+[jcfr]: https://github.com/jcfr
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..d1337f4
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,65 @@
+import nox
+import urllib.request
+import re
+from pathlib import Path
+
+nox.options.sessions = ["lint", "tests"]
+
+ALL_PYTHONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+
+@nox.session
+def lint(session: nox.Session) -> None:
+ """
+ Run the linter.
+ """
+ session.install("pre-commit")
+ session.run(
+ "pre-commit", "run", "--all-files", "--hook-stage=manual", *session.posargs
+ )
+
+
+@nox.session
+def build(session: nox.Session) -> None:
+ """
+ Build an SDist and wheel.
+ """
+
+ session.install("build")
+ session.run("python", "-m", "build")
+
+
+@nox.session
+def update(session: nox.Session) -> None:
+ """
+ Get the latest (or given) version of CMake and update the copy with it.
+ """
+
+ if session.posargs:
+ (version,) = session.posargs
+ else:
+ session.install("lastversion")
+ version = session.run(
+ "lastversion", "kitware/cmake", log=False, silent=True
+ ).strip()
+ session.log(f"CMake {version}")
+
+ cmake_url = f"https://raw.githubusercontent.com/Kitware/CMake/v{version}/Utilities/Sphinx/cmake.py"
+ colors_url = f"https://raw.githubusercontent.com/Kitware/CMake/v{version}/Utilities/Sphinx/colors.py"
+
+ urllib.request.urlretrieve(cmake_url, "sphinxcontrib/moderncmakedomain/cmake.py")
+ urllib.request.urlretrieve(colors_url, "sphinxcontrib/moderncmakedomain/colors.py")
+
+ init_file = Path("sphinxcontrib/moderncmakedomain/__init__.py")
+ txt = init_file.read_text(encoding="utf_8")
+ txt_new = re.sub(r'__version__ = ".*"', f'__version__ = "{version}"', txt)
+ init_file.write_text(txt_new, encoding="utf_8")
+
+
+@nox.session(python=ALL_PYTHONS)
+def tests(session):
+ """
+ Run the unit and regular tests.
+ """
+ # Setuptools is required due to sphinx installing sphinxcontrib extensions that use pkg_resources (fixed upstream but not released yet)
+ session.install(".", "pytest", "setuptools")
+ session.run("pytest", *session.posargs)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..6fbdac9
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,60 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "sphinxcontrib-moderncmakedomain"
+description = "Sphinx Domain for Modern CMake"
+readme = "README.md"
+requires-python = ">=3.7"
+authors = [
+ { name = "Kitware" },
+]
+keywords = [
+ "cmake",
+ "documentation",
+ "kitware",
+ "sphinx",
+ "sphinxcontrib",
+]
+classifiers = [
+ "Environment :: Console",
+ "Environment :: Web Environment",
+ "Framework :: Sphinx :: Extension",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Documentation",
+ "Topic :: Utilities",
+]
+dependencies = ["sphinx>=2"]
+dynamic = ["version"]
+
+[project.urls]
+Homepage = "https://github.com/scikit-build/moderncmakedomain"
+
+[project.optional-dependencies]
+test = ["pytest"]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
+xfail_strict = true
+filterwarnings = [
+ "error",
+ "ignore::DeprecationWarning:sphinx.builders.gettext",
+]
+log_cli_level = "info"
+testpaths = ["tests"]
+
+[tool.hatch]
+version.path = "sphinxcontrib/moderncmakedomain/__init__.py"
+build.targets.wheel.packages = ["sphinxcontrib"]
diff --git a/sphinxcontrib/moderncmakedomain/__init__.py b/sphinxcontrib/moderncmakedomain/__init__.py
new file mode 100644
index 0000000..9a77fde
--- /dev/null
+++ b/sphinxcontrib/moderncmakedomain/__init__.py
@@ -0,0 +1,3 @@
+from .cmake import setup
+
+__version__ = "3.27.0"
diff --git a/sphinxcontrib/moderncmakedomain/cmake.py b/sphinxcontrib/moderncmakedomain/cmake.py
new file mode 100644
index 0000000..66954df
--- /dev/null
+++ b/sphinxcontrib/moderncmakedomain/cmake.py
@@ -0,0 +1,748 @@
+# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+
+# BEGIN imports
+
+import os
+import re
+from dataclasses import dataclass
+from typing import Any, List, Tuple, Type, cast
+
+import sphinx
+
+# The following imports may fail if we don't have Sphinx 2.x or later.
+if sphinx.version_info >= (2,):
+ from docutils import io, nodes
+ from docutils.nodes import Element, Node, TextElement, system_message
+ from docutils.parsers.rst import Directive, directives
+ from docutils.transforms import Transform
+ from docutils.utils.code_analyzer import Lexer, LexerError
+
+ from sphinx import addnodes
+ from sphinx.directives import ObjectDescription, nl_escape_re
+ from sphinx.domains import Domain, ObjType
+ from sphinx.roles import XRefRole
+ from sphinx.util import logging, ws_re
+ from sphinx.util.docutils import ReferenceRole
+ from sphinx.util.nodes import make_refnode
+else:
+ # Sphinx 2.x is required.
+ assert sphinx.version_info >= (2,)
+
+# END imports
+
+# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+# BEGIN pygments tweaks
+
+# Override much of pygments' CMakeLexer.
+# We need to parse CMake syntax definitions, not CMake code.
+
+# For hard test cases that use much of the syntax below, see
+# - module/FindPkgConfig.html
+# (with "glib-2.0>=2.10 gtk+-2.0" and similar)
+# - module/ExternalProject.html
+# (with http:// https:// git@; also has command options -E --build)
+# - manual/cmake-buildsystem.7.html
+# (with nested $<..>; relative and absolute paths, "::")
+
+from pygments.lexer import bygroups # noqa I100
+from pygments.lexers import CMakeLexer
+from pygments.token import (Comment, Name, Number, Operator, Punctuation,
+ String, Text, Whitespace)
+
+# Notes on regular expressions below:
+# - [\.\+-] are needed for string constants like gtk+-2.0
+# - Unix paths are recognized by '/'; support for Windows paths may be added
+# if needed
+# - (\\.) allows for \-escapes (used in manual/cmake-language.7)
+# - $<..$<..$>..> nested occurrence in cmake-buildsystem
+# - Nested variable evaluations are only supported in a limited capacity.
+# Only one level of nesting is supported and at most one nested variable can
+# be present.
+
+CMakeLexer.tokens["root"] = [
+ # fctn(
+ (r'\b(\w+)([ \t]*)(\()',
+ bygroups(Name.Function, Text, Name.Function), '#push'),
+ (r'\(', Name.Function, '#push'),
+ (r'\)', Name.Function, '#pop'),
+ (r'\[', Punctuation, '#push'),
+ (r'\]', Punctuation, '#pop'),
+ (r'[|;,.=*\-]', Punctuation),
+ # used in commands/source_group
+ (r'\\\\', Punctuation),
+ (r'[:]', Operator),
+ # used in FindPkgConfig.cmake
+ (r'[<>]=', Punctuation),
+ # $<...>
+ (r'\$<', Operator, '#push'),
+ # <expr>
+ (r'<[^<|]+?>(\w*\.\.\.)?', Name.Variable),
+ # ${..} $ENV{..}, possibly nested
+ (r'(\$\w*\{)([^\}\$]*)?(?:(\$\w*\{)([^\}]+?)(\}))?([^\}]*?)(\})',
+ bygroups(Operator, Name.Tag, Operator, Name.Tag, Operator, Name.Tag,
+ Operator)),
+ # DATA{ ...}
+ (r'([A-Z]+\{)(.+?)(\})', bygroups(Operator, Name.Tag, Operator)),
+ # URL, git@, ...
+ (r'[a-z]+(@|(://))((\\.)|[\w.+-:/\\])+', Name.Attribute),
+ # absolute path
+ (r'/\w[\w\.\+-/\\]*', Name.Attribute),
+ (r'/', Name.Attribute),
+ # relative path
+ (r'\w[\w\.\+-]*/[\w.+-/\\]*', Name.Attribute),
+ # initial A-Z, contains a-z
+ (r'[A-Z]((\\.)|[\w.+-])*[a-z]((\\.)|[\w.+-])*', Name.Builtin),
+ (r'@?[A-Z][A-Z0-9_]*', Name.Constant),
+ (r'[a-z_]((\\;)|(\\ )|[\w.+-])*', Name.Builtin),
+ (r'[0-9][0-9\.]*', Number),
+ # "string"
+ (r'(?s)"(\\"|[^"])*"', String),
+ (r'\.\.\.', Name.Variable),
+ # <..|..> is different from <expr>
+ (r'<', Operator, '#push'),
+ (r'>', Operator, '#pop'),
+ (r'\n', Whitespace),
+ (r'[ \t]+', Whitespace),
+ (r'#.*\n', Comment),
+ # fallback, for debugging only
+ # (r'[^<>\])\}\|$"# \t\n]+', Name.Exception),
+]
+
+# END pygments tweaks
+
+# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+logger = logging.getLogger(__name__)
+
+# RE to split multiple command signatures.
+sig_end_re = re.compile(r'(?<=[)])\n')
+
+
+@dataclass
+class ObjectEntry:
+ docname: str
+ objtype: str
+ node_id: str
+ name: str
+
+
+class CMakeModule(Directive):
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'encoding': directives.encoding}
+
+ def __init__(self, *args, **keys):
+ self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$')
+ Directive.__init__(self, *args, **keys)
+
+ def run(self):
+ settings = self.state.document.settings
+ if not settings.file_insertion_enabled:
+ raise self.warning(f'{self.name!r} directive disabled.')
+
+ env = self.state.document.settings.env
+ rel_path, path = env.relfn2path(self.arguments[0])
+ path = os.path.normpath(path)
+ encoding = self.options.get('encoding', settings.input_encoding)
+ e_handler = settings.input_encoding_error_handler
+ try:
+ settings.record_dependencies.add(path)
+ f = io.FileInput(source_path=path, encoding=encoding,
+ error_handler=e_handler)
+ except UnicodeEncodeError:
+ msg = (f'Problems with {self.name!r} directive path:\n'
+ f'Cannot encode input file path {path!r} (wrong locale?).')
+ raise self.severe(msg)
+ except IOError as error:
+ msg = f'Problems with {self.name!r} directive path:\n{error}.'
+ raise self.severe(msg)
+ raw_lines = f.read().splitlines()
+ f.close()
+ rst = None
+ lines = []
+ for line in raw_lines:
+ if rst is not None and rst != '#':
+ # Bracket mode: check for end bracket
+ pos = line.find(rst)
+ if pos >= 0:
+ if line[0] == '#':
+ line = ''
+ else:
+ line = line[0:pos]
+ rst = None
+ else:
+ # Line mode: check for .rst start (bracket or line)
+ m = self.re_start.match(line)
+ if m:
+ rst = f']{m.group("eq")}]'
+ line = ''
+ elif line == '#.rst:':
+ rst = '#'
+ line = ''
+ elif rst == '#':
+ if line == '#' or line[:2] == '# ':
+ line = line[2:]
+ else:
+ rst = None
+ line = ''
+ elif rst is None:
+ line = ''
+ lines.append(line)
+ if rst is not None and rst != '#':
+ raise self.warning(f'{self.name!r} found unclosed bracket '
+ f'"#[{rst[1:-1]}[.rst:" in {path!r}')
+ self.state_machine.insert_input(lines, path)
+ return []
+
+
+class _cmake_index_entry:
+ def __init__(self, desc):
+ self.desc = desc
+
+ def __call__(self, title, targetid, main='main'):
+ return ('pair', f'{self.desc} ; {title}', targetid, main, None)
+
+
+_cmake_index_objs = {
+ 'command': _cmake_index_entry('command'),
+ 'cpack_gen': _cmake_index_entry('cpack generator'),
+ 'envvar': _cmake_index_entry('envvar'),
+ 'generator': _cmake_index_entry('generator'),
+ 'genex': _cmake_index_entry('genex'),
+ 'guide': _cmake_index_entry('guide'),
+ 'manual': _cmake_index_entry('manual'),
+ 'module': _cmake_index_entry('module'),
+ 'policy': _cmake_index_entry('policy'),
+ 'prop_cache': _cmake_index_entry('cache property'),
+ 'prop_dir': _cmake_index_entry('directory property'),
+ 'prop_gbl': _cmake_index_entry('global property'),
+ 'prop_inst': _cmake_index_entry('installed file property'),
+ 'prop_sf': _cmake_index_entry('source file property'),
+ 'prop_test': _cmake_index_entry('test property'),
+ 'prop_tgt': _cmake_index_entry('target property'),
+ 'variable': _cmake_index_entry('variable'),
+ }
+
+
+class CMakeTransform(Transform):
+
+ # Run this transform early since we insert nodes we want
+ # treated as if they were written in the documents.
+ default_priority = 210
+
+ def __init__(self, document, startnode):
+ Transform.__init__(self, document, startnode)
+ self.titles = {}
+
+ def parse_title(self, docname):
+ """Parse a document title as the first line starting in [A-Za-z0-9<$]
+ or fall back to the document basename if no such line exists.
+ The cmake --help-*-list commands also depend on this convention.
+ Return the title or False if the document file does not exist.
+ """
+ settings = self.document.settings
+ env = settings.env
+ title = self.titles.get(docname)
+ if title is None:
+ fname = os.path.join(env.srcdir, docname+'.rst')
+ try:
+ f = open(fname, 'r', encoding=settings.input_encoding)
+ except IOError:
+ title = False
+ else:
+ for line in f:
+ if len(line) > 0 and (line[0].isalnum() or
+ line[0] == '<' or line[0] == '$'):
+ title = line.rstrip()
+ break
+ f.close()
+ if title is None:
+ title = os.path.basename(docname)
+ self.titles[docname] = title
+ return title
+
+ def apply(self):
+ env = self.document.settings.env
+
+ # Treat some documents as cmake domain objects.
+ objtype, sep, tail = env.docname.partition('/')
+ make_index_entry = _cmake_index_objs.get(objtype)
+ if make_index_entry:
+ title = self.parse_title(env.docname)
+ # Insert the object link target.
+ if objtype == 'command':
+ targetname = title.lower()
+ elif objtype == 'guide' and not tail.endswith('/index'):
+ targetname = tail
+ else:
+ if objtype == 'genex':
+ m = CMakeXRefRole._re_genex.match(title)
+ if m:
+ title = m.group(1)
+ targetname = title
+ targetid = f'{objtype}:{targetname}'
+ targetnode = nodes.target('', '', ids=[targetid])
+ self.document.note_explicit_target(targetnode)
+ self.document.insert(0, targetnode)
+ # Insert the object index entry.
+ indexnode = addnodes.index()
+ indexnode['entries'] = [make_index_entry(title, targetid)]
+ self.document.insert(0, indexnode)
+
+ # Add to cmake domain object inventory
+ domain = cast(CMakeDomain, env.get_domain('cmake'))
+ domain.note_object(objtype, targetname, targetid, targetid)
+
+
+class CMakeObject(ObjectDescription):
+ def __init__(self, *args, **kwargs):
+ self.targetname = None
+ super().__init__(*args, **kwargs)
+
+ def handle_signature(self, sig, signode):
+ # called from sphinx.directives.ObjectDescription.run()
+ signode += addnodes.desc_name(sig, sig)
+ return sig
+
+ def add_target_and_index(self, name, sig, signode):
+ if self.objtype == 'command':
+ targetname = name.lower()
+ elif self.targetname:
+ targetname = self.targetname
+ else:
+ targetname = name
+ targetid = f'{self.objtype}:{targetname}'
+ if targetid not in self.state.document.ids:
+ signode['names'].append(targetid)
+ signode['ids'].append(targetid)
+ signode['first'] = (not self.names)
+ self.state.document.note_explicit_target(signode)
+
+ domain = cast(CMakeDomain, self.env.get_domain('cmake'))
+ domain.note_object(self.objtype, targetname, targetid, targetid,
+ location=signode)
+
+ make_index_entry = _cmake_index_objs.get(self.objtype)
+ if make_index_entry:
+ self.indexnode['entries'].append(make_index_entry(name, targetid))
+
+
+class CMakeGenexObject(CMakeObject):
+ option_spec = {
+ 'target': directives.unchanged,
+ }
+
+ def handle_signature(self, sig, signode):
+ name = super().handle_signature(sig, signode)
+
+ m = CMakeXRefRole._re_genex.match(sig)
+ if m:
+ name = m.group(1)
+
+ return name
+
+ def run(self):
+ target = self.options.get('target')
+ if target is not None:
+ self.targetname = target
+
+ return super().run()
+
+
+class CMakeSignatureObject(CMakeObject):
+ object_type = 'signature'
+
+ BREAK_ALL = 'all'
+ BREAK_SMART = 'smart'
+ BREAK_VERBATIM = 'verbatim'
+
+ BREAK_CHOICES = {BREAK_ALL, BREAK_SMART, BREAK_VERBATIM}
+
+ def break_option(argument):
+ return directives.choice(argument, CMakeSignatureObject.BREAK_CHOICES)
+
+ option_spec = {
+ 'target': directives.unchanged,
+ 'break': break_option,
+ }
+
+ def _break_signature_all(sig: str) -> str:
+ return ws_re.sub(' ', sig)
+
+ def _break_signature_verbatim(sig: str) -> str:
+ lines = [ws_re.sub('\xa0', line.strip()) for line in sig.split('\n')]
+ return ' '.join(lines)
+
+ def _break_signature_smart(sig: str) -> str:
+ tokens = []
+ for line in sig.split('\n'):
+ token = ''
+ delim = ''
+
+ for c in line.strip():
+ if len(delim) == 0 and ws_re.match(c):
+ if len(token):
+ tokens.append(ws_re.sub('\xa0', token))
+ token = ''
+ else:
+ if c == '[':
+ delim += ']'
+ elif c == '<':
+ delim += '>'
+ elif len(delim) and c == delim[-1]:
+ delim = delim[:-1]
+ token += c
+
+ if len(token):
+ tokens.append(ws_re.sub('\xa0', token))
+
+ return ' '.join(tokens)
+
+ def __init__(self, *args, **kwargs):
+ self.targetnames = {}
+ self.break_style = CMakeSignatureObject.BREAK_SMART
+ super().__init__(*args, **kwargs)
+
+ def get_signatures(self) -> List[str]:
+ content = nl_escape_re.sub('', self.arguments[0])
+ lines = sig_end_re.split(content)
+
+ if self.break_style == CMakeSignatureObject.BREAK_VERBATIM:
+ fixup = CMakeSignatureObject._break_signature_verbatim
+ elif self.break_style == CMakeSignatureObject.BREAK_SMART:
+ fixup = CMakeSignatureObject._break_signature_smart
+ else:
+ fixup = CMakeSignatureObject._break_signature_all
+
+ return [fixup(line.strip()) for line in lines]
+
+ def handle_signature(self, sig, signode):
+ language = 'cmake'
+ classes = ['code', 'cmake', 'highlight']
+
+ node = addnodes.desc_name(sig, '', classes=classes)
+
+ try:
+ tokens = Lexer(sig, language, 'short')
+ except LexerError as error:
+ if self.state.document.settings.report_level > 2:
+ # Silently insert without syntax highlighting.
+ tokens = Lexer(sig, language, 'none')
+ else:
+ raise self.warning(error)
+
+ for classes, value in tokens:
+ if value == '\xa0':
+ node += nodes.inline(value, value, classes=['nbsp'])
+ elif classes:
+ node += nodes.inline(value, value, classes=classes)
+ else:
+ node += nodes.Text(value)
+
+ signode.clear()
+ signode += node
+
+ return sig
+
+ def add_target_and_index(self, name, sig, signode):
+ sig = sig.replace('\xa0', ' ')
+ if sig in self.targetnames:
+ sigargs = self.targetnames[sig]
+ else:
+ def extract_keywords(params):
+ for p in params:
+ if p[0].isalpha():
+ yield p
+ else:
+ return
+
+ keywords = extract_keywords(sig.split('(')[1].split())
+ sigargs = ' '.join(keywords)
+ targetname = sigargs.lower()
+ targetid = nodes.make_id(targetname)
+
+ if targetid not in self.state.document.ids:
+ signode['names'].append(targetname)
+ signode['ids'].append(targetid)
+ signode['first'] = (not self.names)
+ self.state.document.note_explicit_target(signode)
+
+ # Register the signature as a command object.
+ command = sig.split('(')[0].lower()
+ refname = f'{command}({sigargs})'
+ refid = f'command:{command}({targetname})'
+
+ domain = cast(CMakeDomain, self.env.get_domain('cmake'))
+ domain.note_object('command', name=refname, target_id=refid,
+ node_id=targetid, location=signode)
+
+ def run(self):
+ self.break_style = CMakeSignatureObject.BREAK_ALL
+
+ targets = self.options.get('target')
+ if targets is not None:
+ signatures = self.get_signatures()
+ targets = [t.strip() for t in targets.split('\n')]
+ for signature, target in zip(signatures, targets):
+ self.targetnames[signature] = target
+
+ self.break_style = (
+ self.options.get('break', CMakeSignatureObject.BREAK_SMART))
+
+ return super().run()
+
+
+class CMakeReferenceRole:
+ # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
+ _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
+
+ @staticmethod
+ def _escape_angle_brackets(text: str) -> str:
+ # CMake cross-reference targets frequently contain '<' so escape
+ # any explicit `<target>` with '<' not preceded by whitespace.
+ while True:
+ m = CMakeReferenceRole._re.match(text)
+ if m and len(m.group(2)) == 0:
+ text = f'{m.group(1)}\x00<{m.group(3)}>'
+ else:
+ break
+ return text
+
+ def __class_getitem__(cls, parent: Any):
+ class Class(parent):
+ def __call__(self, name: str, rawtext: str, text: str,
+ *args, **kwargs
+ ) -> Tuple[List[Node], List[system_message]]:
+ text = CMakeReferenceRole._escape_angle_brackets(text)
+ return super().__call__(name, rawtext, text, *args, **kwargs)
+ return Class
+
+
+class CMakeCRefRole(CMakeReferenceRole[ReferenceRole]):
+ nodeclass: Type[Element] = nodes.reference
+ innernodeclass: Type[TextElement] = nodes.literal
+ classes: List[str] = ['cmake', 'literal']
+
+ def run(self) -> Tuple[List[Node], List[system_message]]:
+ refnode = self.nodeclass(self.rawtext)
+ self.set_source_info(refnode)
+
+ refnode['refid'] = nodes.make_id(self.target)
+ refnode += self.innernodeclass(self.rawtext, self.title,
+ classes=self.classes)
+
+ return [refnode], []
+
+
+class CMakeXRefRole(CMakeReferenceRole[XRefRole]):
+
+ _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL)
+ _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL)
+ _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
+
+ def __call__(self, typ, rawtext, text, *args, **kwargs):
+ if typ == 'cmake:command':
+ # Translate a CMake command cross-reference of the form:
+ # `command_name(SUB_COMMAND)`
+ # to be its own explicit target:
+ # `command_name(SUB_COMMAND) <command_name(SUB_COMMAND)>`
+ # so the XRefRole `fix_parens` option does not add more `()`.
+ m = CMakeXRefRole._re_sub.match(text)
+ if m:
+ text = f'{text} <{text}>'
+ elif typ == 'cmake:genex':
+ m = CMakeXRefRole._re_genex.match(text)
+ if m:
+ text = f'{text} <{m.group(1)}>'
+ elif typ == 'cmake:guide':
+ m = CMakeXRefRole._re_guide.match(text)
+ if m:
+ text = f'{m.group(2)} <{text}>'
+ return super().__call__(typ, rawtext, text, *args, **kwargs)
+
+ # We cannot insert index nodes using the result_nodes method
+ # because CMakeXRefRole is processed before substitution_reference
+ # nodes are evaluated so target nodes (with 'ids' fields) would be
+ # duplicated in each evaluated substitution replacement. The
+ # docutils substitution transform does not allow this. Instead we
+ # use our own CMakeXRefTransform below to add index entries after
+ # substitutions are completed.
+ #
+ # def result_nodes(self, document, env, node, is_ref):
+ # pass
+
+
+class CMakeXRefTransform(Transform):
+
+ # Run this transform early since we insert nodes we want
+ # treated as if they were written in the documents, but
+ # after the sphinx (210) and docutils (220) substitutions.
+ default_priority = 221
+
+ # This helper supports docutils < 0.18, which is missing 'findall',
+ # and docutils == 0.18.0, which is missing 'traverse'.
+ def _document_findall_as_list(self, condition):
+ if hasattr(self.document, 'findall'):
+ # Fully iterate into a list so the caller can grow 'self.document'
+ # while iterating.
+ return list(self.document.findall(condition))
+
+ # Fallback to 'traverse' on old docutils, which returns a list.
+ return self.document.traverse(condition)
+
+ def apply(self):
+ env = self.document.settings.env
+
+ # Find CMake cross-reference nodes and add index and target
+ # nodes for them.
+ for ref in self._document_findall_as_list(addnodes.pending_xref):
+ if not ref['refdomain'] == 'cmake':
+ continue
+
+ objtype = ref['reftype']
+ make_index_entry = _cmake_index_objs.get(objtype)
+ if not make_index_entry:
+ continue
+
+ objname = ref['reftarget']
+ if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname):
+ # Do not index cross-references to guide sections.
+ continue
+
+ if objtype == 'command':
+ # Index signature references to their parent command.
+ objname = objname.split('(')[0].lower()
+
+ targetnum = env.new_serialno(f'index-{objtype}:{objname}')
+
+ targetid = f'index-{targetnum}-{objtype}:{objname}'
+ targetnode = nodes.target('', '', ids=[targetid])
+ self.document.note_explicit_target(targetnode)
+
+ indexnode = addnodes.index()
+ indexnode['entries'] = [make_index_entry(objname, targetid, '')]
+ ref.replace_self([indexnode, targetnode, ref])
+
+
+class CMakeDomain(Domain):
+ """CMake domain."""
+ name = 'cmake'
+ label = 'CMake'
+ object_types = {
+ 'command': ObjType('command', 'command'),
+ 'cpack_gen': ObjType('cpack_gen', 'cpack_gen'),
+ 'envvar': ObjType('envvar', 'envvar'),
+ 'generator': ObjType('generator', 'generator'),
+ 'genex': ObjType('genex', 'genex'),
+ 'guide': ObjType('guide', 'guide'),
+ 'variable': ObjType('variable', 'variable'),
+ 'module': ObjType('module', 'module'),
+ 'policy': ObjType('policy', 'policy'),
+ 'prop_cache': ObjType('prop_cache', 'prop_cache'),
+ 'prop_dir': ObjType('prop_dir', 'prop_dir'),
+ 'prop_gbl': ObjType('prop_gbl', 'prop_gbl'),
+ 'prop_inst': ObjType('prop_inst', 'prop_inst'),
+ 'prop_sf': ObjType('prop_sf', 'prop_sf'),
+ 'prop_test': ObjType('prop_test', 'prop_test'),
+ 'prop_tgt': ObjType('prop_tgt', 'prop_tgt'),
+ 'manual': ObjType('manual', 'manual'),
+ }
+ directives = {
+ 'command': CMakeObject,
+ 'envvar': CMakeObject,
+ 'genex': CMakeGenexObject,
+ 'signature': CMakeSignatureObject,
+ 'variable': CMakeObject,
+ # Other `object_types` cannot be created except by the `CMakeTransform`
+ }
+ roles = {
+ 'cref': CMakeCRefRole(),
+ 'command': CMakeXRefRole(fix_parens=True, lowercase=True),
+ 'cpack_gen': CMakeXRefRole(),
+ 'envvar': CMakeXRefRole(),
+ 'generator': CMakeXRefRole(),
+ 'genex': CMakeXRefRole(),
+ 'guide': CMakeXRefRole(),
+ 'variable': CMakeXRefRole(),
+ 'module': CMakeXRefRole(),
+ 'policy': CMakeXRefRole(),
+ 'prop_cache': CMakeXRefRole(),
+ 'prop_dir': CMakeXRefRole(),
+ 'prop_gbl': CMakeXRefRole(),
+ 'prop_inst': CMakeXRefRole(),
+ 'prop_sf': CMakeXRefRole(),
+ 'prop_test': CMakeXRefRole(),
+ 'prop_tgt': CMakeXRefRole(),
+ 'manual': CMakeXRefRole(),
+ }
+ initial_data = {
+ 'objects': {}, # fullname -> ObjectEntry
+ }
+
+ def clear_doc(self, docname):
+ to_clear = set()
+ for fullname, obj in self.data['objects'].items():
+ if obj.docname == docname:
+ to_clear.add(fullname)
+ for fullname in to_clear:
+ del self.data['objects'][fullname]
+
+ def merge_domaindata(self, docnames, otherdata):
+ """Merge domaindata from the workers/chunks when they return.
+
+ Called once per parallelization chunk.
+ Only used when sphinx is run in parallel mode.
+
+ :param docnames: a Set of the docnames that are part of the current
+ chunk to merge
+ :param otherdata: the partial data calculated by the current chunk
+ """
+ for refname, obj in otherdata['objects'].items():
+ if obj.docname in docnames:
+ self.data['objects'][refname] = obj
+
+ def resolve_xref(self, env, fromdocname, builder,
+ typ, target, node, contnode):
+ targetid = f'{typ}:{target}'
+ obj = self.data['objects'].get(targetid)
+
+ if obj is None and typ == 'command':
+ # If 'command(args)' wasn't found, try just 'command'.
+ # TODO: remove this fallback? warn?
+ # logger.warning(f'no match for {targetid}')
+ command = target.split('(')[0]
+ targetid = f'{typ}:{command}'
+ obj = self.data['objects'].get(targetid)
+
+ if obj is None:
+ # TODO: warn somehow?
+ return None
+
+ return make_refnode(builder, fromdocname, obj.docname, obj.node_id,
+ contnode, target)
+
+ def note_object(self, objtype: str, name: str, target_id: str,
+ node_id: str, location: Any = None):
+ if target_id in self.data['objects']:
+ other = self.data['objects'][target_id].docname
+ logger.warning(
+ f'CMake object {target_id!r} also described in {other!r}',
+ location=location)
+
+ self.data['objects'][target_id] = ObjectEntry(
+ self.env.docname, objtype, node_id, name)
+
+ def get_objects(self):
+ for refname, obj in self.data['objects'].items():
+ yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
+
+
+def setup(app):
+ app.add_directive('cmake-module', CMakeModule)
+ app.add_transform(CMakeTransform)
+ app.add_transform(CMakeXRefTransform)
+ app.add_domain(CMakeDomain)
+ return {"parallel_read_safe": True}
diff --git a/sphinxcontrib/moderncmakedomain/colors.py b/sphinxcontrib/moderncmakedomain/colors.py
new file mode 100644
index 0000000..dae0063
--- /dev/null
+++ b/sphinxcontrib/moderncmakedomain/colors.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+
+from pygments.style import Style
+from pygments.token import Name, Comment, String, Number, Operator, Whitespace
+
+class CMakeTemplateStyle(Style):
+ """
+ for more token names, see pygments/styles.default
+ """
+
+ background_color = "#f8f8f8"
+ default_style = ""
+
+ styles = {
+ Whitespace: "#bbbbbb",
+ Comment: "italic #408080",
+ Operator: "#555555",
+ String: "#217A21",
+ Number: "#105030",
+ Name.Builtin: "#333333", # anything lowercase
+ Name.Function: "#007020", # function
+ Name.Variable: "#1080B0", # <..>
+ Name.Tag: "#bb60d5", # ${..}
+ Name.Constant: "#4070a0", # uppercase only
+ Name.Entity: "italic #70A020", # @..@
+ Name.Attribute: "#906060", # paths, URLs
+ Name.Label: "#A0A000", # anything left over
+ Name.Exception: "bold #FF0000", # for debugging only
+ }
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..6a0c552
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,9 @@
+import pytest
+
+from sphinx.testing.path import path
+
+pytest_plugins = 'sphinx.testing.fixtures'
+
+@pytest.fixture(scope="session")
+def rootdir():
+ return path(__file__).parent.abspath() / "roots"
diff --git a/tests/roots/test-root/conf.py b/tests/roots/test-root/conf.py
new file mode 100644
index 0000000..0aa6266
--- /dev/null
+++ b/tests/roots/test-root/conf.py
@@ -0,0 +1 @@
+extensions = ["sphinxcontrib.moderncmakedomain"]
diff --git a/tests/roots/test-root/external.rst b/tests/roots/test-root/external.rst
new file mode 100644
index 0000000..f09b874
--- /dev/null
+++ b/tests/roots/test-root/external.rst
@@ -0,0 +1,5 @@
+External
+--------
+
+
+An external reference is :cmake:command:`find_program`.
diff --git a/tests/roots/test-root/index.rst b/tests/roots/test-root/index.rst
new file mode 100644
index 0000000..0257f55
--- /dev/null
+++ b/tests/roots/test-root/index.rst
@@ -0,0 +1,7 @@
+.. toctree::
+
+ local
+ external
+ parallel
+ padding
+ more
diff --git a/tests/roots/test-root/local.rst b/tests/roots/test-root/local.rst
new file mode 100644
index 0000000..8efcf20
--- /dev/null
+++ b/tests/roots/test-root/local.rst
@@ -0,0 +1,17 @@
+Local
+-----
+
+
+Some CMake
+==========
+
+.. cmake:variable:: MYVAR
+
+.. cmake:variable:: MYVAR2
+
+.. cmake:command:: somecommand
+
+Some references
+===============
+
+Using :cmake:variable:`MYVAR`. Also :cmake:command:`somecommand`.
diff --git a/tests/roots/test-root/more.rst b/tests/roots/test-root/more.rst
new file mode 100644
index 0000000..8572267
--- /dev/null
+++ b/tests/roots/test-root/more.rst
@@ -0,0 +1,5 @@
+More
+----
+
+
+Even more padding to get to 5 pages.
diff --git a/tests/roots/test-root/padding.rst b/tests/roots/test-root/padding.rst
new file mode 100644
index 0000000..222decf
--- /dev/null
+++ b/tests/roots/test-root/padding.rst
@@ -0,0 +1,4 @@
+Padding
+-------
+
+Sphinx requires 5 or more pages to enable parallel builds.
diff --git a/tests/roots/test-root/parallel.rst b/tests/roots/test-root/parallel.rst
new file mode 100644
index 0000000..1f81e4f
--- /dev/null
+++ b/tests/roots/test-root/parallel.rst
@@ -0,0 +1,16 @@
+
+Parallel
+--------
+
+An extra file to make sure parallel is tested.
+
+Some CMake
+==========
+
+.. cmake:variable:: OTHERVAR
+
+Some references
+===============
+
+
+Using :cmake:variable:`OTHERVAR`.
diff --git a/tests/test_basic.py b/tests/test_basic.py
new file mode 100644
index 0000000..659bed4
--- /dev/null
+++ b/tests/test_basic.py
@@ -0,0 +1,27 @@
+import pytest
+
+@pytest.mark.parametrize("parallel", [0, 1, 2])
+@pytest.mark.sphinx(
+ "html",
+ freshenv=True,
+ confoverrides={"html_baseurl": "https://example.org/docs/", "language": "en"},
+)
+def test_simple_html(app, status, warning, parallel):
+ app.warningiserror = True
+ app.parallel = parallel
+ app.build()
+ local_pth = app.outdir / "local.html"
+ external_pth = app.outdir / "external.html"
+
+ with open(str(local_pth), encoding="utf-8") as f:
+ local = f.read()
+
+ with open(str(external_pth), encoding="utf-8") as f:
+ external = f.read()
+
+ print(local)
+ assert 'href="#variable:MYVAR"' in local
+ assert 'id="variable:MYVAR"' in local
+
+
+
diff --git a/tests/test_version.py b/tests/test_version.py
new file mode 100644
index 0000000..7b1c89b
--- /dev/null
+++ b/tests/test_version.py
@@ -0,0 +1,7 @@
+"""Version tests."""
+from sphinxcontrib.moderncmakedomain import __version__
+
+
+def test_version():
+ """Test that version has at least 3 parts."""
+ assert __version__.count('.') >= 2