Moved sphinx files

This commit is contained in:
2025-03-20 12:52:55 +01:00
parent 6868b0d8ba
commit d5f10276ee
17 changed files with 10 additions and 10 deletions

2
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
assets/img/*
generated/*

41
docs/Makefile Normal file
View File

@@ -0,0 +1,41 @@
# Minimal Makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment
SPHINXOPTS ?= -c .
SPHINXBUILD ?= sphinx-build
SPHINX_SOURCE_DIR ?= ../
SPHINX_BUILD_DIR ?= ./generated
SPHINX_APIDOC_BUILD_DIR = $(SPHINX_SOURCE_DIR)modules
.PHONY: help install copy-images apidoc remove-apidoc html Makefile
# Copy images before running any Sphinx command (except for help)
copy-images:
@echo "Copying images from ../assets/img/ to ./assets/img/..."
cp -vr ../assets/img/* ./assets/img/
# Generate reStructuredText files from Python modules using sphinx-apidoc
apidoc:
@echo "Running sphinx-apidoc..."
sphinx-apidoc -f -o $(SPHINX_APIDOC_BUILD_DIR) $(SPHINX_SOURCE_DIR)
remove-apidoc:
@echo "Removing sphinx-apidoc files..."
- rm -rv $(SPHINX_APIDOC_BUILD_DIR)
# "help" target does not copy images
help:
@$(SPHINXBUILD) -M help "$(SPHINX_SOURCE_DIR)" "$(SPHINX_BUILD_DIR)" $(SPHINXOPTS) $(O)
# HTML target depends on apidoc so that sphinx-apidoc runs first
html: copy-images
@$(SPHINXBUILD) -M html "$(SPHINX_SOURCE_DIR)" "$(SPHINX_BUILD_DIR)" $(SPHINXOPTS) $(O)
clean: remove-apidoc
@$(SPHINXBUILD) -M html "$(SPHINX_SOURCE_DIR)" "$(SPHINX_BUILD_DIR)" $(SPHINXOPTS) $(O)
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SPHINX_SOURCE_DIR)" "$(SPHINX_BUILD_DIR)" $(SPHINXOPTS) $(O)

56
docs/README.md Normal file
View File

@@ -0,0 +1,56 @@
# Documentation
CyMaIS uses [Sphinx](https://www.sphinx-doc.org/) to automatically generate its documentation and leverages the [Awesome Sphinx Theme](https://sphinxawesome.xyz/) for a sleek and responsive design. Enjoy a seamless, visually engaging experience 🚀✨.
## For Users
You can access the documentation [here](https://docs.cymais.cloud/) 🔗. Browse the latest updates and guides to get started.
## For Administrators
### Setup
#### On Localhost
To generate the documentation locally, run the following command:
```bash
pkgmgr shell cymais -c "make refresh"
```
This command performs the following steps:
- **Copy Images:** Before building, it copies the necessary image assets from `../assets/img/` to `./assets/img/` using the `copy-images` target.
- **Generate API Documentation:** It executes `sphinx-apidoc` (via the `apidoc` target) to automatically generate reStructuredText files for all Python modules. These files are stored under a designated directory (e.g., `modules`), ensuring that every Python file is included in the documentation.
- **Build HTML Documentation:** Finally, it builds the HTML documentation using `sphinx-build` (triggered by the `html` target).
Once complete, you can view the documentation at the output location (e.g., [templates/html/index.html](templates/html/index.html)) 👀💻.
#### On Server
The same commands can be used on the server to ensure that documentation is always up to date. Make sure the server environment is properly configured with the necessary Python packages and assets.
### Additional Commands
- **`make copy-images`:**
Copies image files from the assets directory into the local documentation directory. This ensures that all required images are available for the generated documentation.
- **`make apidoc`:**
Runs `sphinx-apidoc` to scan all Python files in the source directory and generate corresponding reStructuredText files. This automates the inclusion of all Python modules into the Sphinx documentation.
- **`make html`:**
This target depends on the `apidoc` target. It first generates the API documentation and then builds the HTML documentation using `sphinx-build`. This is the standard target to produce the final, viewable documentation.
- **`make refresh`:**
A custom target (typically defined as a combination of cleaning the previous build and then running `make html`) that ensures the documentation is regenerated from scratch with the latest changes.
### Debug
To debug and produce a log file, execute:
```bash
pkgmgr shell cymais -c "make refresh SPHINXOPTS='-v -c .' 2>&1 | tee debug.log"
```
This command increases the verbosity of the Sphinx build process and redirects all output to `debug.log`, which is useful for troubleshooting any issues during the documentation build.
```

View File

@@ -0,0 +1,102 @@
document.addEventListener("DOMContentLoaded", function() {
// Initialization: wait for window load and then trigger current nav detection.
window.addEventListener("load", function() {
console.log("Window loaded, initializing current nav...");
initCurrentNav();
});
// Re-trigger when the hash changes.
window.addEventListener("hashchange", function() {
console.log("Hash changed, reinitializing current nav...");
initCurrentNav();
});
function initCurrentNav() {
// If Alpine.js is available and provides nextTick, use it.
if (window.Alpine && typeof window.Alpine.nextTick === 'function') {
window.Alpine.nextTick(processNav);
} else {
processNav();
}
}
function processNav() {
var currentHash = window.location.hash;
console.log("initCurrentNav: Current hash:", currentHash);
if (!currentHash) return;
// Select all internal links within the .current-index container.
var links = document.querySelectorAll('.current-index a.reference.internal');
links.forEach(function(link) {
var href = link.getAttribute("href");
console.log("initCurrentNav: Checking link:", href);
// If the link is hash-only (e.g. "#setup-guide")
if (href && href.trim().startsWith("#")) {
if (href.trim() === currentHash.trim()) {
console.log("initCurrentNav: Match found for hash-only link:", href);
document.querySelectorAll('.current-index a.reference.internal.current').forEach(function(link) {
link.classList.remove("current");
});
link.classList.add("current");
markAsCurrent(link);
}
}
// Otherwise, if the link includes a file and a hash, compare the hash part.
else if (href && href.indexOf('#') !== -1) {
var parts = href.split('#');
var linkHash = "#" + parts[1].trim();
console.log("initCurrentNav: Extracted link hash:", linkHash);
if (linkHash === currentHash.trim()) {
console.log("initCurrentNav: Match found for link with file and hash:", href);
markAsCurrent(link);
}
}
else {
console.log("initCurrentNav: No match for link:", href);
}
});
// After processing links, open submenus only for those li elements marked as current.
openCurrentSubmenus();
}
// Mark the link's parent li and all its ancestor li elements as current.
function markAsCurrent(link) {
var li = link.closest("li");
if (!li) {
console.log("markAsCurrent: No parent li found for link:", link);
return;
}
li.classList.add("current");
console.log("markAsCurrent: Marked li as current:", li);
// If Alpine.js is used, set its "expanded" property to true.
if (li.__x && li.__x.$data) {
li.__x.$data.expanded = true;
console.log("markAsCurrent: Set Alpine expanded on li:", li);
}
// Propagate upward: mark all ancestor li elements as current.
var parentLi = li.parentElement.closest("li");
while (parentLi) {
parentLi.classList.add("current");
if (parentLi.__x && parentLi.__x.$data) {
parentLi.__x.$data.expanded = true;
}
console.log("markAsCurrent: Propagated current to ancestor li:", parentLi);
parentLi = parentLi.parentElement.closest("li");
}
}
// Open immediate submenu elements (the direct children with x-show) of li.current.
function openCurrentSubmenus() {
document.querySelectorAll('.current-index li.current').forEach(function(li) {
// Only target immediate child elements that have x-show.
li.querySelectorAll(":scope > [x-show]").forEach(function(elem) {
if (elem.style.display === "none" || elem.style.display === "") {
elem.style.display = "block";
console.log("openCurrentSubmenus: Opened submenu element:", elem);
}
});
});
}
window.initCurrentNav = initCurrentNav;
});

103
docs/conf.py Normal file
View File

@@ -0,0 +1,103 @@
import sys
import logging
# Check if a verbose flag is present in the command line arguments.
if any(arg in sys.argv for arg in ["-v", "--verbose"]):
logging_level = logging.DEBUG
else:
logging_level = logging.INFO
logging.basicConfig(level=logging_level)
import os
sys.path.insert(0, os.path.abspath('.'))
project = 'CyMaIS - Cyber Master Infrastructure Solution'
copyright = '2025, Kevin Veen-Birkenbach'
author = 'Kevin Veen-Birkenbach'
# Highlighting for Jinja
from sphinx.highlighting import lexers
from pygments.lexers.templates import DjangoLexer
lexers['jinja'] = DjangoLexer()
lexers['j2'] = DjangoLexer()
# -- General configuration ---------------------------------------------------
templates_path = ['templates']
exclude_patterns = ['docs', 'venv', 'venv/**']
# -- Options for HTML output -------------------------------------------------
html_theme = 'sphinxawesome_theme'
html_static_path = ['assets']
html_sidebars = {
'**': [
'logo.html',
'structure.html', # Include your custom template
]
}
cymais_logo = "assets/img/logo.png"
html_favicon = "assets/img/favicon.ico"
html_theme_options = {
"show_prev_next": False,
"logo_light": cymais_logo,
"logo_dark": cymais_logo,
}
source_suffix = {
'.rst': 'restructuredtext',
'.md': 'markdown',
}
sys.path.insert(0, os.path.abspath('./extensions'))
extensions = [
'sphinx.ext.autosummary',
'sphinx.ext.autodoc',
'myst_parser',
'extensions.local_file_headings',
'extensions.local_subfolders',
'extensions.roles_overview',
'extensions.markdown_include',
'sphinx.ext.autodoc',
'sphinx.ext.napoleon', # Optional, wenn Sie Google- oder NumPy-Dokstrings verwenden
]
autosummary_generate = True
myst_enable_extensions = [
"colon_fence",
]
import logging
from docutils import nodes
logger = logging.getLogger(__name__)
def replace_assets_in_doctree(app, doctree, docname):
# Replace asset references in image nodes
for node in doctree.traverse(nodes.image):
if "assets/" in node['uri']:
new_uri = node['uri'].replace("assets/", "_static/")
node['uri'] = new_uri
logger.info("Replaced image URI in {}: {}".format(docname, new_uri))
# Replace asset references in raw HTML nodes
for node in doctree.traverse(nodes.raw):
if node.get('format') == 'html' and "assets/" in node.astext():
new_text = node.astext().replace("assets/", "_static/")
node.children = [nodes.raw('', new_text, format='html')]
logger.info("Replaced raw HTML assets in {}.".format(docname))
def setup(app):
app.connect("doctree-resolved", replace_assets_in_doctree)
python_domain = app.registry.domains.get('py')
if python_domain is not None:
directive = python_domain.directives.get('currentmodule')
if directive is not None:
directive.optional_arguments = 10
return {'version': '1.0', 'parallel_read_safe': True}

View File

View File

@@ -0,0 +1,61 @@
import os
import sys
import logging as std_logging # Use the standard logging module
from sphinx.util import logging # Sphinx logging is used elsewhere if needed
from docutils.parsers.rst import Directive
from .nav_utils import natural_sort_key, extract_headings_from_file, group_headings, sort_tree, MAX_HEADING_LEVEL, DEFAULT_MAX_NAV_DEPTH
# Set up our logger based on command-line args.
logger = std_logging.getLogger(__name__)
if any(arg in sys.argv for arg in ["-v", "--verbose"]):
logger.setLevel(std_logging.DEBUG)
else:
logger.setLevel(std_logging.INFO)
DEFAULT_MAX_NAV_DEPTH = 4
def add_local_file_headings(app, pagename, templatename, context, doctree):
logger.debug("add_local_file_headings called with pagename: %s", pagename)
srcdir = app.srcdir
directory = os.path.dirname(pagename)
abs_dir = os.path.join(srcdir, directory)
if not os.path.isdir(abs_dir):
logger.warning("Directory %s not found for page %s.", abs_dir, pagename)
context['local_md_headings'] = []
return
# Get only files with .md or .rst extensions.
files = [f for f in os.listdir(abs_dir) if f.endswith('.md') or f.endswith('.rst')]
# If an index file is present, remove any readme files (case-insensitive).
files_lower = [f.lower() for f in files]
if 'index.rst' in files_lower:
files = [f for f in files if f.lower() not in ['readme.md']]
file_items = []
for file in files:
filepath = os.path.join(abs_dir, file)
headings = extract_headings_from_file(filepath, max_level=MAX_HEADING_LEVEL)
basename, _ = os.path.splitext(file)
# Set priority: index gets priority 0, otherwise 1.
priority = 0 if basename.lower() == 'index' else 1
for heading in headings:
file_link = os.path.join(directory, basename)
file_items.append({
'level': heading['level'],
'text': heading['text'],
'link': file_link,
'anchor': heading['anchor'],
'priority': priority,
'filename': basename
})
tree = group_headings(file_items)
sort_tree(tree)
logger.debug("Generated tree: %s", tree)
context['local_md_headings'] = tree
def setup(app):
app.add_config_value('local_nav_max_depth', DEFAULT_MAX_NAV_DEPTH, 'env')
app.connect('html-page-context', add_local_file_headings)
return {'version': '0.1', 'parallel_read_safe': True}

View File

@@ -0,0 +1,130 @@
import os
from sphinx.util import logging
from .nav_utils import extract_headings_from_file, MAX_HEADING_LEVEL
logger = logging.getLogger(__name__)
CANDIDATES = ['index.rst', 'readme.md', 'main.rst']
def collect_folder_tree(dir_path, base_url):
"""
Recursively collects the folder tree starting from the given directory.
For each folder:
- Hidden folders (names starting with a dot) are skipped.
- A folder is processed only if it contains one of the representative files:
index.rst, index.md, readme.md, or readme.rst.
- The first heading of the representative file is used as the folder title.
- The representative file is not listed as a file in the folder.
- All other Markdown and reStructuredText files are listed without sub-headings,
using their first heading as the file title.
"""
# Skip hidden directories
if os.path.basename(dir_path).startswith('.'):
return None
# List all files in the current directory with .md or .rst extension
files = [f for f in os.listdir(dir_path)
if os.path.isfile(os.path.join(dir_path, f))
and (f.endswith('.md') or f.endswith('.rst'))]
# Find representative file for folder title using index or readme
rep_file = None
for candidate in CANDIDATES:
for f in files:
if f.lower() == candidate:
rep_file = f
break
if rep_file:
break
# Skip this folder if no representative file exists
if not rep_file:
return None
rep_path = os.path.join(dir_path, rep_file)
headings = extract_headings_from_file(rep_path, max_level=MAX_HEADING_LEVEL)
folder_title = headings[0]['text'] if headings else os.path.basename(dir_path)
folder_link = os.path.join(base_url, os.path.splitext(rep_file)[0])
# Remove the representative file from the list to avoid duplication,
# and filter out any additional "readme.md" or "index.rst" files.
files.remove(rep_file)
files = [f for f in files if f.lower() not in CANDIDATES]
# Process the remaining files in the current directory
file_items = []
for file in sorted(files, key=lambda s: s.lower()):
file_path = os.path.join(dir_path, file)
file_headings = extract_headings_from_file(file_path, max_level=MAX_HEADING_LEVEL)
file_title = file_headings[0]['text'] if file_headings else file
file_base = os.path.splitext(file)[0]
file_link = os.path.join(base_url, file_base)
file_items.append({
'level': 1,
'text': file_title,
'link': file_link,
'anchor': '',
'priority': 1,
'filename': file
})
# Process subdirectories (ignoring hidden ones)
dir_items = []
for item in sorted(os.listdir(dir_path), key=lambda s: s.lower()):
full_path = os.path.join(dir_path, item)
if os.path.isdir(full_path) and not item.startswith('.'):
subtree = collect_folder_tree(full_path, os.path.join(base_url, item))
if subtree:
dir_items.append(subtree)
# Combine files and subdirectories as children of the current folder
children = file_items + dir_items
return {
'text': folder_title,
'link': folder_link,
'children': children,
'filename': os.path.basename(dir_path)
}
def mark_current(node, active):
"""
Recursively mark nodes as current if the active page (pagename)
matches the node's link or is a descendant of it.
The function sets node['current'] = True if:
- The node's link matches the active page exactly, or
- The active page begins with the node's link plus a separator (indicating a child).
Additionally, if any child node is current, the parent is marked as current.
"""
is_current = False
node_link = node.get('link', '').rstrip('/')
active = active.rstrip('/')
if node_link and (active == node_link or active.startswith(node_link + '/')):
is_current = True
# Recurse into children if they exist
children = node.get('children', [])
for child in children:
if mark_current(child, active):
is_current = True
node['current'] = is_current
return is_current
def add_local_subfolders(app, pagename, templatename, context, doctree):
"""
Sets the 'local_subfolders' context variable with the entire folder tree
starting from app.srcdir, and marks the tree with the 'current' flag up
to the active page.
"""
root_dir = app.srcdir
folder_tree = collect_folder_tree(root_dir, '')
if folder_tree:
mark_current(folder_tree, pagename)
context['local_subfolders'] = [folder_tree] if folder_tree else []
def setup(app):
app.connect('html-page-context', add_local_subfolders)
return {'version': '0.1', 'parallel_read_safe': True}

View File

@@ -0,0 +1,80 @@
import os
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.util import logging
logger = logging.getLogger(__name__)
from myst_parser.parsers.sphinx_ import MystParser
class MarkdownIncludeDirective(Directive):
required_arguments = 1 # Path to the Markdown file
optional_arguments = 0
final_argument_whitespace = True
has_content = False
def run(self):
logger.info("Executing markdown-include directive")
env = self.state.document.settings.env
# Determine the absolute path of the file.
rel_filename, filename = env.relfn2path(self.arguments[0])
logger.info("Markdown file: %s", filename)
if not os.path.exists(filename):
error = self.state_machine.reporter.error(
f'File not found: {filename}',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
try:
with open(filename, 'r', encoding='utf-8') as f:
markdown_content = f.read()
except Exception as e:
error = self.state_machine.reporter.error(
f'Error reading file {filename}: {e}',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
# Parse the Markdown content with MystParser.
parser = MystParser()
from docutils.frontend import OptionParser
from docutils.utils import new_document
settings = OptionParser(components=(MystParser,)).get_default_values()
# Attach the Sphinx environment to the settings so that myst_parser works.
settings.env = self.state.document.settings.env
doc = new_document(filename, settings=settings)
parser.parse(markdown_content, doc)
logger.info("Markdown parsing completed successfully")
# Remove the first header (title) if it exists.
if doc.children:
first_section = doc.children[0]
if isinstance(first_section, nodes.section) and first_section.children:
first_child = first_section.children[0]
if isinstance(first_child, nodes.title):
# If there are additional children, remove the title node.
if len(first_section.children) > 1:
first_section.pop(0)
logger.info("Removed first header from Markdown content")
else:
# If it's the only child, clear its content instead.
first_child.clear()
logger.info("Cleared text of first header from Markdown content")
# Unwrap the first section if it no longer has a title.
if isinstance(first_section, nodes.section):
has_title = any(isinstance(child, nodes.title) and child.astext().strip()
for child in first_section.children)
if not has_title:
# Remove the section wrapper so that its content does not create a TOC entry.
unwrapped = list(first_section.children)
# Replace the first section with its children.
doc.children = unwrapped + doc.children[1:]
logger.info("Unwrapped first section to avoid a TOC entry")
return doc.children
def setup(app):
app.add_directive("markdown-include", MarkdownIncludeDirective)
return {'version': '0.1', 'parallel_read_safe': True}

View File

@@ -0,0 +1,78 @@
import os
import re
import yaml
DEFAULT_MAX_NAV_DEPTH = 4
MAX_HEADING_LEVEL = 0 # This can be overridden in your configuration
def natural_sort_key(text):
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', text)]
def extract_headings_from_file(filepath, max_level=MAX_HEADING_LEVEL):
# If max_level is 0, set it to a very high value to effectively iterate infinitely
if max_level == 0:
max_level = 9999
headings = []
ext = os.path.splitext(filepath)[1].lower()
try:
with open(filepath, 'r', encoding='utf-8') as f:
if ext == '.md':
in_code_block = False
for line in f:
if line.strip().startswith("```"):
in_code_block = not in_code_block
continue
if in_code_block:
continue
# Assuming markdown headings are defined with '#' characters
match = re.match(r'^(#{1,})(.*?)$', line)
if match:
level = len(match.group(1))
if level <= max_level:
heading_text = match.group(2).strip()
anchor = re.sub(r'\s+', '-', heading_text.lower())
anchor = re.sub(r'[^a-z0-9\-]', '', anchor)
headings.append({'level': level, 'text': heading_text, 'anchor': anchor})
elif ext == '.rst':
lines = f.readlines()
for i in range(len(lines) - 1):
text_line = lines[i].rstrip("\n")
underline = lines[i+1].rstrip("\n")
if len(underline) >= 3 and re.fullmatch(r'[-=~\^\+"\'`]+', underline):
level = 1
heading_text = text_line.strip()
headings.append({'level': level, 'text': heading_text, 'anchor': ''})
except Exception as e:
print(f"Warning: Error reading {filepath}: {e}")
if not headings:
base = os.path.basename(filepath).lower()
if base == 'index.rst':
folder = os.path.dirname(filepath)
readme_path = os.path.join(folder, 'README.md')
if os.path.isfile(readme_path):
try:
headings = extract_headings_from_file(readme_path, max_level)
except Exception as e:
print(f"Warning: Error reading fallback README.md in {folder}: {e}")
return headings
def group_headings(headings):
tree = []
stack = []
for heading in headings:
heading['children'] = []
while stack and stack[-1]['level'] >= heading['level']:
stack.pop()
if stack:
stack[-1]['children'].append(heading)
else:
tree.append(heading)
stack.append(heading)
return tree
def sort_tree(tree):
tree.sort(key=lambda x: (x.get('priority', 1), natural_sort_key(x.get('filename', x['text']))))
for item in tree:
if item.get('children'):
sort_tree(item['children'])

View File

@@ -0,0 +1,116 @@
import os
import glob
import re
import yaml
from docutils import nodes
from sphinx.util import logging
from docutils.parsers.rst import Directive
logger = logging.getLogger(__name__)
class RolesOverviewDirective(Directive):
"""
A directive to embed a roles overview as reStructuredText.
It scans the roles directory (i.e. every folder under "roles") for a "meta/main.yml" file,
reads the roles galaxy tags and description, and outputs an overview grouped by each tag.
For each role, it attempts to extract a level1 heading from its README.md as the title.
If no title is found, the role folder name is used.
The title is rendered as a clickable link to the role's README.md.
"""
has_content = False
def run(self):
env = self.state.document.settings.env
srcdir = env.srcdir
roles_dir = os.path.join(srcdir, 'roles')
if not os.path.isdir(roles_dir):
logger.warning(f"Roles directory not found: {roles_dir}")
error_node = self.state.document.reporter.error(
"Roles directory not found.", line=self.lineno)
return [error_node]
# Gather role entries grouped by tag.
categories = {}
for role_path in glob.glob(os.path.join(roles_dir, '*')):
if os.path.isdir(role_path):
meta_path = os.path.join(role_path, 'meta', 'main.yml')
if os.path.exists(meta_path):
try:
with open(meta_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
except Exception as e:
logger.warning(f"Error reading YAML file {meta_path}: {e}")
continue
role_name = os.path.basename(role_path)
# Determine title from README.md if present.
readme_path = os.path.join(role_path, 'README.md')
title = role_name
if os.path.exists(readme_path):
try:
with open(readme_path, 'r', encoding='utf-8') as f:
for line in f:
match = re.match(r'^#\s+(.*)$', line)
if match:
title = match.group(1).strip()
break
except Exception as e:
logger.warning(f"Error reading README.md for {role_name}: {e}")
galaxy_info = data.get('galaxy_info', {})
tags = galaxy_info.get('galaxy_tags', [])
if not tags:
tags = ['uncategorized']
role_description = galaxy_info.get('description', '')
role_entry = {
'name': role_name,
'title': title,
'description': role_description,
'link': f'roles/{role_name}/README.md',
'tags': tags,
}
for tag in tags:
categories.setdefault(tag, []).append(role_entry)
else:
logger.warning(f"meta/main.yml not found for role {role_path}")
# Sort categories and roles alphabetically.
sorted_categories = sorted(categories.items(), key=lambda x: x[0].lower())
for tag, roles in sorted_categories:
roles.sort(key=lambda r: r['name'].lower())
# Build document structure.
container = nodes.container()
# For each category, create a section to serve as a large category heading.
for tag, roles in sorted_categories:
# Create a section for the category.
cat_id = nodes.make_id(tag)
category_section = nodes.section(ids=[cat_id])
category_title = nodes.title(text=tag)
category_section += category_title
# For each role within the category, create a subsection.
for role in roles:
role_section_id = nodes.make_id(role['title'])
role_section = nodes.section(ids=[role_section_id])
# Create a title node with a clickable reference.
role_title = nodes.title()
reference = nodes.reference(text=role['title'], refuri=role['link'])
role_title += reference
role_section += role_title
if role['description']:
para = nodes.paragraph(text=role['description'])
role_section += para
category_section += role_section
container += category_section
return [container]
def setup(app):
app.add_directive("roles-overview", RolesOverviewDirective)
return {'version': '0.1', 'parallel_read_safe': True}

5
docs/requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
myst-parser
sphinx
sphinxawesome-theme
docutils
sphinxcontrib-jinja

5
docs/templates/logo.html vendored Normal file
View File

@@ -0,0 +1,5 @@
<div class="sidebar-logo" style="text-align: center; margin-bottom: 1em;">
<img src="{{ pathto("_static/img/logo.png", 1) }}" alt="Logo" style="max-width: 100%;">
</div>

65
docs/templates/structure.html vendored Normal file
View File

@@ -0,0 +1,65 @@
{% macro render_headings(headings, level=1) %}
<ul class="toctree-l{{ level }}" style="list-style: none; padding-left: 0; overflow-x: auto; white-space: nowrap;">
{% for item in headings %}
<li class="toctree-l{{ level }}{% if item.current %} current{% endif %}"
{% if item.children %}
x-data="{ expanded: {{ 'true' if item.current else 'false' }} }"
{% endif %}
style="white-space: nowrap;">
<div class="menu-item" style="display: inline-flex; align-items: center; justify-content: space-between; width: 100%; white-space: nowrap;">
<!-- Link and file open section -->
<div style="display: inline-flex; align-items: center; white-space: nowrap;">
<a class="reference internal{% if item.children %} expandable{% endif %}{% if item.current and not item.children %} current{% endif %}"
href="{{ pathto(item.link).replace('#', '') }}{% if item.anchor %}#{{ item.anchor }}{% endif %}"
style="text-decoration: none; white-space: nowrap;">
{{ item.text }}
</a>
</div>
<!-- Expand-Toggle Button -->
{% if item.children %}
<button @click.prevent.stop="expanded = !expanded" type="button" class="toggle-button"
style="background: none; border: none; padding: 0; margin-left: auto;">
<span x-show="!expanded">
<svg fill="currentColor" height="18px" stroke="none" viewBox="0 0 24 24" width="18px"
xmlns="http://www.w3.org/2000/svg">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path>
</svg>
</span>
<span x-show="expanded">&#9660;</span>
</button>
{% endif %}
</div>
{% if item.children %}
<div x-show="expanded">
{{ render_headings(item.children, level+1) }}
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
{% if local_md_headings or local_subfolders %}
<div class="local-md-headings">
{% if local_md_headings %}
<div class="current-index" x-data x-init="typeof initCurrentNav === 'function' && initCurrentNav()">
<p class="caption" role="heading">
<span class="caption-text">Current Index</span>
</p>
{{ render_headings(local_md_headings) }}
<br />
</div>
{% endif %}
{% if local_subfolders %}
<div class="full-index">
<p class="caption" role="heading">
<span class="caption-text">Full Index</span>
</p>
{{ render_headings(local_subfolders) }}
<br />
</div>
{% endif %}
</div>
{% endif %}
<script src="{{ pathto('_static/js/current-nav.js', 1) }}"></script>