Merge branch 'docs-sphinx-tool-directive' into docs

develop
lethosor 2022-08-09 11:36:11 -04:00
commit 4f799a152a
No known key found for this signature in database
GPG Key ID: 76A269552F4F58C1
9 changed files with 244 additions and 108 deletions

@ -14,15 +14,15 @@ serve to show the default.
# pylint:disable=redefined-builtin
import contextlib
import datetime
import io
import os
import re
import shlex # pylint:disable=unused-import
import sphinx
import sys
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'docs', 'sphinx_extensions'))
from dfhack.util import write_file_if_changed
if os.environ.get('DFHACK_DOCS_BUILD_OFFLINE'):
# block attempted image downloads, particularly for the PDF builder
@ -40,67 +40,6 @@ if os.environ.get('DFHACK_DOCS_BUILD_OFFLINE'):
requests.get = request_disabled
# -- Support :dfhack-keybind:`command` ------------------------------------
# this is a custom directive that pulls info from default keybindings
from docutils import nodes
from docutils.parsers.rst import roles
def get_keybinds(root, files, keybindings):
"""Add keybindings in the specified files to the
given keybindings dict.
"""
for file in files:
with open(os.path.join(root, file)) as f:
lines = [l.replace('keybinding add', '').strip() for l in f.readlines()
if l.startswith('keybinding add')]
for k in lines:
first, command = k.split(' ', 1)
bind, context = (first.split('@') + [''])[:2]
if ' ' not in command:
command = command.replace('"', '')
tool = command.split(' ')[0].replace('"', '')
keybindings[tool] = keybindings.get(tool, []) + [
(command, bind.split('-'), context)]
def get_all_keybinds(root_dir):
"""Get the implemented keybinds, and return a dict of
{tool: [(full_command, keybinding, context), ...]}.
"""
keybindings = dict()
for root, _, files in os.walk(root_dir):
get_keybinds(root, files, keybindings)
return keybindings
KEYBINDS = get_all_keybinds('data/init')
# pylint:disable=unused-argument,dangerous-default-value,too-many-arguments
def dfhack_keybind_role_func(role, rawtext, text, lineno, inliner,
options={}, content=[]):
"""Custom role parser for DFHack default keybinds."""
roles.set_classes(options)
if text not in KEYBINDS:
return [], []
newnode = nodes.paragraph()
for cmd, key, ctx in KEYBINDS[text]:
n = nodes.paragraph()
newnode += n
n += nodes.strong('Keybinding:', 'Keybinding:')
n += nodes.inline(' ', ' ')
for k in key:
n += nodes.inline(k, k, classes=['kbd'])
if cmd != text:
n += nodes.inline(' -> ', ' -> ')
n += nodes.literal(cmd, cmd, classes=['guilabel'])
if ctx:
n += nodes.inline(' in ', ' in ')
n += nodes.literal(ctx, ctx)
return [newnode], []
roles.register_canonical_role('dfhack-keybind', dfhack_keybind_role_func)
# -- Autodoc for DFhack plugins and scripts -------------------------------
def doc_dir(dirname, files, prefix):
@ -145,23 +84,6 @@ def get_tags():
return tags
@contextlib.contextmanager
def write_file_if_changed(path):
with io.StringIO() as buffer:
yield buffer
new_contents = buffer.getvalue()
try:
with open(path, 'r') as infile:
old_contents = infile.read()
except IOError:
old_contents = None
if old_contents != new_contents:
with open(path, 'w') as outfile:
outfile.write(new_contents)
def generate_tag_indices():
os.makedirs('docs/tags', mode=0o755, exist_ok=True)
with write_file_if_changed('docs/tags/index.rst') as topidx:
@ -204,29 +126,12 @@ def write_tool_docs():
outfile.write(include)
def all_keybinds_documented():
"""Check that all keybindings are documented with the :dfhack-keybind:
directive somewhere."""
undocumented_binds = set(KEYBINDS)
tools = set(i[0] for i in DOC_ALL_DIRS)
for t in tools:
with open(('./docs/tools/{}.rst').format(t)) as f:
tool_binds = set(re.findall(':dfhack-keybind:`(.*?)`', f.read()))
undocumented_binds -= tool_binds
if undocumented_binds:
raise ValueError('The following DFHack commands have undocumented '
'keybindings: {}'.format(sorted(undocumented_binds)))
# Actually call the docs generator and run test
write_tool_docs()
generate_tag_indices()
#all_keybinds_documented() # comment out while we're transitioning
# -- General configuration ------------------------------------------------
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'docs', 'sphinx_extensions'))
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = '1.8'
@ -237,6 +142,7 @@ extensions = [
'sphinx.ext.extlinks',
'dfhack.changelog',
'dfhack.lexer',
'dfhack.tool_docs',
]
sphinx_major_version = sphinx.version_info[0]

@ -1,7 +1,8 @@
3dveins
=======
**Tags:** `tag/fort`, `tag/mod`, `tag/map`
:dfhack-keybind:`3dveins`
.. dfhack-tool::
:tags: fort mod map
:index:`Rewrite layer veins to expand in 3D space.
<3dveins; Rewrite layer veins to expand in 3D space.>` Existing, flat veins

@ -1,9 +1,12 @@
autodump
========
**Tags:** `tag/fort`, `tag/auto`, `tag/fps`, `tag/items`, `tag/stockpiles`
:dfhack-keybind:`autodump`
:dfhack-keybind:`autodump-destroy-here`
:dfhack-keybind:`autodump-destroy-item`
.. dfhack-tool::
:tags: fort auto fps items stockpiles
.. dfhack-command:: autodump-destroy-here
.. dfhack-command:: autodump-destroy-item
:index:`Automatically set items in a stockpile to be dumped.
<autodump (plugin); Automatically set items in a stockpile to be dumped.>` When

@ -1,7 +1,11 @@
autogems
========
**Tags:** `tag/fort`, `tag/auto`, `tag/jobs`
:dfhack-keybind:`autogems-reload`
.. dfhack-tool:: autogems
:tags: fort auto jobs
:no-command:
.. dfhack-command:: autogems-reload
:index:`Automatically cut rough gems. <autogems; Automatically cut rough gems.>`
This plugin periodically scans your stocks of rough gems and creates manager

@ -1,6 +1,9 @@
automelt
========
**Tags:** `tag/fort`, `tag/auto`, `tag/items`, `tag/stockpiles`
.. dfhack-tool::
:tags: fort auto items stockpiles
:no-command:
:index:`Quickly designate items to be melted.
<automelt; Quickly designate items to be melted.>` When `enabled <enable>`, this

@ -6,7 +6,7 @@ import sys
from sphinx.errors import ExtensionError, SphinxError, SphinxWarning
from dfhack.util import DFHACK_ROOT, DOCS_ROOT
from dfhack.util import DFHACK_ROOT, DOCS_ROOT, write_file_if_changed
CHANGELOG_PATHS = (
'docs/changelog.txt',
@ -172,7 +172,7 @@ def consolidate_changelog(all_entries):
def print_changelog(versions, all_entries, path, replace=True, prefix=''):
# all_entries: version -> section -> entry
with open(path, 'w') as f:
with write_file_if_changed(path) as f:
def write(line):
if replace:
line = replace_text(line, REPLACEMENTS)

@ -0,0 +1,175 @@
# useful references:
# https://www.sphinx-doc.org/en/master/extdev/appapi.html
# https://www.sphinx-doc.org/en/master/development/tutorials/recipe.html
# https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-directives
import logging
import os
from typing import List
import docutils.nodes as nodes
import docutils.parsers.rst.directives as rst_directives
import sphinx
import sphinx.addnodes as addnodes
import sphinx.directives
import dfhack.util
logger = sphinx.util.logging.getLogger(__name__)
_KEYBINDS = {}
_KEYBINDS_RENDERED = set() # commands whose keybindings have been rendered
def scan_keybinds(root, files, keybindings):
"""Add keybindings in the specified files to the
given keybindings dict.
"""
for file in files:
with open(os.path.join(root, file)) as f:
lines = [l.replace('keybinding add', '').strip() for l in f.readlines()
if l.startswith('keybinding add')]
for k in lines:
first, command = k.split(' ', 1)
bind, context = (first.split('@') + [''])[:2]
if ' ' not in command:
command = command.replace('"', '')
tool = command.split(' ')[0].replace('"', '')
keybindings[tool] = keybindings.get(tool, []) + [
(command, bind.split('-'), context)]
def scan_all_keybinds(root_dir):
"""Get the implemented keybinds, and return a dict of
{tool: [(full_command, keybinding, context), ...]}.
"""
keybindings = dict()
for root, _, files in os.walk(root_dir):
scan_keybinds(root, files, keybindings)
return keybindings
def render_dfhack_keybind(command) -> List[nodes.paragraph]:
_KEYBINDS_RENDERED.add(command)
out = []
if command not in _KEYBINDS:
return out
for keycmd, key, ctx in _KEYBINDS[command]:
n = nodes.paragraph()
n += nodes.strong('Keybinding:', 'Keybinding:')
n += nodes.inline(' ', ' ')
for k in key:
n += nodes.inline(k, k, classes=['kbd'])
if keycmd != command:
n += nodes.inline(' -> ', ' -> ')
n += nodes.literal(keycmd, keycmd, classes=['guilabel'])
if ctx:
n += nodes.inline(' in ', ' in ')
n += nodes.literal(ctx, ctx)
out.append(n)
return out
def check_missing_keybinds():
# FIXME: _KEYBINDS_RENDERED is empty in the parent process under parallel builds
# consider moving to a sphinx Domain to solve this properly
for missing_command in sorted(set(_KEYBINDS.keys()) - _KEYBINDS_RENDERED):
logger.warning('Undocumented keybindings for command: %s', missing_command)
# pylint:disable=unused-argument,dangerous-default-value,too-many-arguments
def dfhack_keybind_role(role, rawtext, text, lineno, inliner,
options={}, content=[]):
"""Custom role parser for DFHack default keybinds."""
return render_dfhack_keybind(text), []
class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription):
has_content = False
required_arguments = 0
optional_arguments = 1
def get_name_or_docname(self):
if self.arguments:
return self.arguments[0]
else:
return self.env.docname.split('/')[-1]
@staticmethod
def make_labeled_paragraph(label, content, label_class=nodes.strong, content_class=nodes.inline) -> nodes.paragraph:
return nodes.paragraph('', '', *[
label_class('', '{}:'.format(label)),
nodes.inline(text=' '),
content_class('', content),
])
@staticmethod
def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition:
return nodes.topic('', *children, classes=['dfhack-tool-summary'])
def render_content(self) -> List[nodes.Node]:
raise NotImplementedError
def run(self):
return [self.wrap_box(*self.render_content())]
class DFHackToolDirective(DFHackToolDirectiveBase):
option_spec = {
'tags': dfhack.util.directive_arg_str_list,
'no-command': rst_directives.flag,
}
def render_content(self) -> List[nodes.Node]:
tag_nodes = [nodes.strong(text='Tags:'), nodes.inline(text=' ')]
for tag in self.options.get('tags', []):
tag_nodes += [
addnodes.pending_xref(tag, nodes.inline(text=tag), **{
'reftype': 'ref',
'refdomain': 'std',
'reftarget': 'tag/' + tag,
'refexplicit': False,
'refwarn': True,
}),
nodes.inline(text=' | '),
]
tag_nodes.pop()
return [
nodes.paragraph('', '', *tag_nodes),
]
def run(self):
out = DFHackToolDirectiveBase.run(self)
if 'no-command' not in self.options:
out += [self.wrap_box(*DFHackCommandDirective.render_content(self))]
return out
class DFHackCommandDirective(DFHackToolDirectiveBase):
def render_content(self) -> List[nodes.Node]:
command = self.get_name_or_docname()
return [
self.make_labeled_paragraph('Command', command, content_class=nodes.literal),
*render_dfhack_keybind(command),
]
def register(app):
app.add_directive('dfhack-tool', DFHackToolDirective)
app.add_directive('dfhack-command', DFHackCommandDirective)
app.add_role('dfhack-keybind', dfhack_keybind_role)
_KEYBINDS.update(scan_all_keybinds(os.path.join(dfhack.util.DFHACK_ROOT, 'data', 'init')))
def setup(app):
app.connect('builder-inited', register)
# TODO: re-enable once detection is corrected
# app.connect('build-finished', lambda *_: check_missing_keybinds())
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

@ -1,3 +1,5 @@
import contextlib
import io
import os
DFHACK_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
@ -5,3 +7,34 @@ DOCS_ROOT = os.path.join(DFHACK_ROOT, 'docs')
if not os.path.isdir(DOCS_ROOT):
raise ReferenceError('docs root not found: %s' % DOCS_ROOT)
@contextlib.contextmanager
def write_file_if_changed(path):
with io.StringIO() as buffer:
yield buffer
new_contents = buffer.getvalue()
try:
with open(path, 'r') as infile:
old_contents = infile.read()
except IOError:
old_contents = None
if old_contents != new_contents:
with open(path, 'w') as outfile:
outfile.write(new_contents)
# directive argument helpers (supplementing docutils.parsers.rst.directives)
def directive_arg_str_list(argument):
"""
Converts a space- or comma-separated list of values into a Python list
of strings.
(Directive option conversion function.)
"""
if ',' in argument:
entries = argument.split(',')
else:
entries = argument.split()
return [entry.strip() for entry in entries]

@ -60,3 +60,14 @@ div.body {
span.pre {
overflow-wrap: break-word;
}
div.dfhack-tool-summary {
margin: 10px 0;
padding: 10px 15px;
}
div.dfhack-tool-summary p {
margin-top: 0;
margin-bottom: 0.5em;
line-height: 1em;
}