Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sphinx documentation framework #19950

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/docbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Documentation Build

on:
push:
branches:
- master

permissions:
contents: write

jobs:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout the code
uses: actions/checkout@v2
with:
path: matter
fetch-depth: 0
- name: Install Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: cache-pip
uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-doc-pip
- name: Install base dependencies
working-directory: matter
run: |
sudo apt update
sudo pip3 install -U pip
pip3 install -r docs/requirements.txt
- name: Build documentation
working-directory: matter/docs
run: |
mkdir -p _build/src
make html
touch _build/html/.nojekyll
- name: Deploy to gh-pages
if: github.repository == 'project-chip/connectedhomeip'
uses: peaceiris/actions-gh-pages@v3
gautesl marked this conversation as resolved.
Show resolved Hide resolved
with:
deploy_key: ${{ secrets.DOXYGEN_DEPLOY_KEY }}
external_repository: project-chip/connectedhomeip-doc
publish_dir: matter/docs/_build/html
# Keep only the latest version of the documentation
force_orphan: true
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ __pycache__
*.pyc
*.egg-info

# Doxygen outputs
docs/html
# Python venv
.venv

# Documentation
docs/_build

# VSCode java extensions
.project
Expand Down
6 changes: 5 additions & 1 deletion .spellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ matrix:
# ```python
# content
# ```
- open: '(?s)^(?P<open> *`{3,})[a-z]*$'
#
# Allow MyST extended syntax like:
# ```{include} my/file.md
# ```
- open: '(?s)^(?P<open> *`{3,})([a-z]*$|{[a-z]*?}\s*[^\n]*?$)'
close: '^(?P=open)$'
# Ignore text between inline back ticks
- open: '(?P<open>`+)'
Expand Down
2 changes: 1 addition & 1 deletion docs/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@ INPUT = README.md \
docs/guides/BUILDING.md \
docs/VSCODE_DEVELOPMENT.md \
docs/PROJECT_FLOW.md \
docs/STYLE_GUIDE.md \
docs/style/style_guide.md \
src/ble \
src/controller \
src/crypto \
Expand Down
21 changes: 21 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?= -W -c . -d _build/doctrees
SPHINXBUILD ?= sphinx-build
SOURCEDIR = _build/src
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
mkdir -p "$(SOURCEDIR)"
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
## Style Guide

- Documentation about style is documented in
[the style guide](./STYLE_GUIDE.md)
[the style guide](./style/style_guide.md)
- Additional documentation about more specific files are in the
[style folder](./style/)

Expand Down
241 changes: 241 additions & 0 deletions docs/_extensions/external_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
"""
External content
################

Copyright (c) 2021 Nordic Semiconductor ASA
SPDX-License-Identifier: Apache-2.0

Introduction
============

This extension allows to import sources from directories out of the Sphinx
source directory. They are copied to the source directory before starting the
build. Note that the copy is *smart*, that is, only updated files are actually
copied. Therefore, incremental builds detect changes correctly and behave as
expected.

Links to external content not included in the generated documentation are
transformed to external links as needed.

Configuration options
=====================

- ``external_content_contents``: A list of external contents. Each entry is
a tuple with two fields: the external base directory and a file glob pattern.
- ``external_content_link_prefixes``: A list of link prefixes out of scope.
All links to content with these prefixes are made external.
- ``external_content_link_extensions``: A list of file extensions in scope of
the documentation. All links to content without these file extensions are
made external.
- ``external_content_keep``: A list of file globs (relative to the destination
directory) that should be kept even if they do not exist in the source
directory. This option can be useful for auto-generated files in the
destination directory.
"""

import filecmp
import os
from pathlib import Path
import re
import shutil
import tempfile
from typing import Dict, Any, List, Optional

from sphinx.application import Sphinx

__version__ = "0.1.0"

DIRECTIVES = ("figure", "image", "include", "literalinclude")
"""Default directives for included content."""

EXTERNAL_LINK_URL_PREFIX = (
"https://github.com/project-chip/connectedhomeip/blob/master/"
)


def adjust_includes(
fname: Path,
basepath: Path,
encoding: str,
link_prefixes: List[str],
extensions: List[str],
dstpath: Optional[Path] = None,
) -> None:
"""Adjust included content paths.

Args:
fname: File to be processed.
basepath: Base path to be used to resolve content location.
encoding: Sources encoding.
link_prefixes: Prefixes of links that are made external.
extensions: Filename extensions links to which are not made external.
dstpath: Destination path for fname if its path is not the actual destination.
"""

if fname.suffix != ".md":
return

dstpath = dstpath or fname.parent

def _adjust_path(path):
# ignore absolute paths, section links, hyperlinks and same folder
if path.startswith(("/", "#", "http", "www")) or not "/" in path:
return path
return Path(os.path.relpath(basepath / path, dstpath)).as_posix()

def _adjust_links(m):
displayed, fpath = m.groups()
fpath_adj = _adjust_path(fpath)
return f"[{displayed}]({fpath_adj})"

def _adjust_external(m):
displayed, target = m.groups()
return f"[{displayed}]({EXTERNAL_LINK_URL_PREFIX}{target})"

def _adjust_filetype(m):
displayed, target, extension = m.groups()
if extension.lower() in extensions or target.startswith("http"):
return m.group(0)

return f"[{displayed}]({EXTERNAL_LINK_URL_PREFIX}{target})"

def _remove_section_links(m):
(file_link,) = m.groups()
return file_link + ")"

def _adjust_image_link(m):
prefix, fpath, postfix = m.groups()
fpath_adj = _adjust_path(fpath)
return f"{prefix}{fpath_adj}{postfix}"

rules = [
# Find any links and adjust the path
(r"\[([^\[\]]*)\]\s*\((.*)\)", _adjust_links),

# Find links that lead to an external folder and transform it
# into an external link.
(
r"\[([^\[\]]*)\]\s*\((?:\.\./)*((?:" + "|".join(link_prefixes) + r")[^)]*)\)",
_adjust_external,
),

# Find links that lead to a section within another file and
# remove the section part of the link.
(r"(\[[^\[\]]*\]\s*\([^)]*\.md)#.*\)", _remove_section_links),

# Find links that lead to a non-presentable filetype and transform
# it into an external link.
(
r"\[([^\[\]]*)\]\s*\((?:\.\./)*((?:[^()]+?/)*[^.()]+?(\.[^)/]+))\)",
_adjust_filetype,
),

# Find links that lead to a folder and transform it into an external link.
(
r"\[([^\[\]]*)\]\s*\((?:\.\./)*((?:[^()]+?/)+[^).#/]+)(\))",
_adjust_filetype,
),

# Find image links in img tags and adjust them
(r"(<img [^>]*src=[\"'])([^ >]+)([\"'][^>]*>)", _adjust_image_link)
]

with open(fname, "r+", encoding=encoding) as f:
content = f.read()
modified = False

for pattern, sub_func in rules:
content, changes_made = re.subn(pattern, sub_func, content)
modified = modified or changes_made

if modified:
f.seek(0)
f.write(content)
f.truncate()


def sync_contents(app: Sphinx) -> None:
"""Synhronize external contents.

Args:
app: Sphinx application instance.
"""

srcdir = Path(app.srcdir).resolve()
to_copy = []
to_delete = set(f for f in srcdir.glob("**/*") if not f.is_dir())
to_keep = set(
f
for k in app.config.external_content_keep
for f in srcdir.glob(k)
if not f.is_dir()
)

for content in app.config.external_content_contents:
prefix_src, glob = content
for src in prefix_src.glob(glob):
if src.is_dir():
to_copy.extend(
[(f, prefix_src) for f in src.glob("**/*") if not f.is_dir()]
)
else:
to_copy.append((src, prefix_src))

for entry in to_copy:
src, prefix_src = entry
dst = (srcdir / src.relative_to(prefix_src)).resolve()

if dst in to_delete:
to_delete.remove(dst)

if not dst.parent.exists():
dst.parent.mkdir(parents=True)

# just copy if it does not exist
if not dst.exists():
shutil.copy(src, dst)
adjust_includes(
dst,
src.parent,
app.config.source_encoding,
app.config.external_content_link_prefixes,
app.config.external_content_link_extensions,
)
# if origin file is modified only copy if different
elif src.stat().st_mtime > dst.stat().st_mtime:
with tempfile.TemporaryDirectory() as td:
# adjust origin includes before comparing
src_adjusted = Path(td) / src.name
shutil.copy(src, src_adjusted)
adjust_includes(
src_adjusted,
src.parent,
app.config.source_encoding,
app.config.external_content_link_prefixes,
app.config.external_content_link_extensions,
dstpath=dst.parent,
)

if not filecmp.cmp(src_adjusted, dst):
dst.unlink()
shutil.move(os.fspath(src_adjusted), os.fspath(dst))

# remove any previously copied file not present in the origin folder,
# excepting those marked to be kept.
for file in to_delete - to_keep:
file.unlink()


def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value("external_content_contents", [], "env")
app.add_config_value("external_content_keep", [], "")
app.add_config_value("external_content_link_prefixes", [], "env")
app.add_config_value("external_content_link_extensions", [], "env")

app.connect("builder-inited", sync_contents)

return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}
Binary file added docs/_static/images/favicon.ico
Binary file not shown.
Binary file added docs/_static/images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# API

```{toctree}
:glob:

*
```
Loading