2022-07-27 00:17:21 -06:00
|
|
|
# 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
|
|
|
|
|
2022-09-23 10:32:08 -06:00
|
|
|
from collections import defaultdict
|
2022-08-07 00:16:38 -06:00
|
|
|
import logging
|
2022-08-06 23:37:14 -06:00
|
|
|
import os
|
2022-09-21 18:53:51 -06:00
|
|
|
import re
|
2022-09-23 09:10:37 -06:00
|
|
|
from typing import Dict, Iterable, List, Optional, Tuple, Type
|
2022-08-06 23:03:15 -06:00
|
|
|
|
2022-07-27 00:17:21 -06:00
|
|
|
import docutils.nodes as nodes
|
2022-09-20 23:51:44 -06:00
|
|
|
from docutils.nodes import Node
|
2022-08-06 23:03:15 -06:00
|
|
|
import docutils.parsers.rst.directives as rst_directives
|
2022-07-27 00:17:21 -06:00
|
|
|
import sphinx
|
2022-08-06 15:26:33 -06:00
|
|
|
import sphinx.addnodes as addnodes
|
2022-07-27 00:17:21 -06:00
|
|
|
import sphinx.directives
|
2022-09-21 18:53:51 -06:00
|
|
|
from sphinx.domains import Domain, Index, IndexEntry
|
2022-09-20 23:51:44 -06:00
|
|
|
from sphinx.util.docutils import SphinxDirective
|
|
|
|
from sphinx.util.nodes import process_index_entry
|
2022-07-27 00:17:21 -06:00
|
|
|
|
|
|
|
import dfhack.util
|
|
|
|
|
2022-08-06 23:37:14 -06:00
|
|
|
|
2022-08-07 00:16:38 -06:00
|
|
|
logger = sphinx.util.logging.getLogger(__name__)
|
|
|
|
|
2022-08-17 20:37:03 -06:00
|
|
|
|
2022-08-17 21:17:08 -06:00
|
|
|
def get_label_class(builder: sphinx.builders.Builder) -> Type[nodes.Inline]:
|
|
|
|
if builder.format == 'text':
|
|
|
|
return nodes.inline
|
|
|
|
else:
|
|
|
|
return nodes.strong
|
|
|
|
|
2022-08-17 20:37:03 -06:00
|
|
|
def make_labeled_paragraph(label: Optional[str]=None, content: Optional[str]=None,
|
|
|
|
label_class=nodes.strong, content_class=nodes.inline) -> nodes.paragraph:
|
|
|
|
p = nodes.paragraph('', '')
|
|
|
|
if label is not None:
|
|
|
|
p += [
|
|
|
|
label_class('', '{}:'.format(label)),
|
|
|
|
nodes.inline('', ' '),
|
|
|
|
]
|
|
|
|
if content is not None:
|
|
|
|
p += content_class('', content)
|
|
|
|
return p
|
|
|
|
|
2022-08-25 22:46:23 -06:00
|
|
|
def make_summary(builder: sphinx.builders.Builder, summary: str) -> nodes.paragraph:
|
|
|
|
para = nodes.paragraph('', '')
|
|
|
|
if builder.format == 'text':
|
|
|
|
# It might be clearer to block indent instead of just indenting the
|
|
|
|
# first line, but this is clearer than nothing.
|
|
|
|
para += nodes.inline(text=' ')
|
|
|
|
para += nodes.inline(text=summary)
|
|
|
|
return para
|
|
|
|
|
2022-08-06 23:37:14 -06:00
|
|
|
_KEYBINDS = {}
|
2022-08-07 00:16:38 -06:00
|
|
|
_KEYBINDS_RENDERED = set() # commands whose keybindings have been rendered
|
2022-08-06 23:37:14 -06:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2022-08-17 21:17:08 -06:00
|
|
|
def render_dfhack_keybind(command, builder: sphinx.builders.Builder) -> List[nodes.paragraph]:
|
2022-08-07 00:16:38 -06:00
|
|
|
_KEYBINDS_RENDERED.add(command)
|
2022-08-06 23:51:38 -06:00
|
|
|
out = []
|
2022-08-06 23:37:14 -06:00
|
|
|
if command not in _KEYBINDS:
|
2022-08-06 23:51:38 -06:00
|
|
|
return out
|
2022-08-06 23:37:14 -06:00
|
|
|
for keycmd, key, ctx in _KEYBINDS[command]:
|
2022-08-17 21:17:08 -06:00
|
|
|
n = make_labeled_paragraph('Keybinding', label_class=get_label_class(builder))
|
2022-08-06 23:37:14 -06:00
|
|
|
for k in key:
|
2022-08-17 21:18:58 -06:00
|
|
|
if builder.format == 'text':
|
|
|
|
k = '[{}]'.format(k)
|
2022-08-06 23:37:14 -06:00
|
|
|
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)
|
2022-08-06 23:51:38 -06:00
|
|
|
out.append(n)
|
|
|
|
return out
|
2022-08-06 23:37:14 -06:00
|
|
|
|
|
|
|
|
2022-08-07 00:16:38 -06:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2022-08-06 21:08:51 -06:00
|
|
|
class DFHackToolDirectiveBase(sphinx.directives.ObjectDescription):
|
2022-07-27 00:17:21 -06:00
|
|
|
has_content = False
|
2022-08-06 14:24:56 -06:00
|
|
|
required_arguments = 0
|
2022-08-06 21:12:26 -06:00
|
|
|
optional_arguments = 1
|
2022-07-27 00:17:21 -06:00
|
|
|
|
2022-08-06 21:08:51 -06:00
|
|
|
def get_name_or_docname(self):
|
2022-08-06 14:24:56 -06:00
|
|
|
if self.arguments:
|
2022-08-06 21:08:51 -06:00
|
|
|
return self.arguments[0]
|
2022-08-06 14:24:56 -06:00
|
|
|
else:
|
2022-09-15 21:03:34 -06:00
|
|
|
parts = self.env.docname.split('/')
|
|
|
|
if 'tools' in parts:
|
|
|
|
return '/'.join(parts[parts.index('tools') + 1:])
|
|
|
|
else:
|
|
|
|
return parts[-1]
|
2022-08-06 21:08:51 -06:00
|
|
|
|
2022-09-23 11:10:28 -06:00
|
|
|
def add_index_entry(self, name, tag) -> None:
|
|
|
|
indexdata = (name, self.options.get('summary', ''), '', self.env.docname, '', 0)
|
|
|
|
self.env.domaindata[tag]['objects'].append(indexdata)
|
|
|
|
|
2022-08-06 23:03:15 -06:00
|
|
|
@staticmethod
|
|
|
|
def wrap_box(*children: List[nodes.Node]) -> nodes.Admonition:
|
2022-08-08 00:29:22 -06:00
|
|
|
return nodes.topic('', *children, classes=['dfhack-tool-summary'])
|
2022-08-06 23:03:15 -06:00
|
|
|
|
2022-08-17 21:17:08 -06:00
|
|
|
def make_labeled_paragraph(self, *args, **kwargs):
|
|
|
|
# convenience wrapper to set label_class to the desired builder-specific node type
|
|
|
|
kwargs.setdefault('label_class', get_label_class(self.env.app.builder))
|
|
|
|
return make_labeled_paragraph(*args, **kwargs)
|
|
|
|
|
2022-08-06 23:03:15 -06:00
|
|
|
def render_content(self) -> List[nodes.Node]:
|
2022-08-06 21:08:51 -06:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def run(self):
|
2022-08-06 23:03:15 -06:00
|
|
|
return [self.wrap_box(*self.render_content())]
|
2022-07-27 00:17:21 -06:00
|
|
|
|
2022-08-06 21:08:51 -06:00
|
|
|
|
|
|
|
class DFHackToolDirective(DFHackToolDirectiveBase):
|
|
|
|
option_spec = {
|
|
|
|
'tags': dfhack.util.directive_arg_str_list,
|
2022-08-06 23:03:15 -06:00
|
|
|
'no-command': rst_directives.flag,
|
2022-08-09 23:37:24 -06:00
|
|
|
'summary': rst_directives.unchanged,
|
2022-08-06 21:08:51 -06:00
|
|
|
}
|
|
|
|
|
2022-08-06 23:03:15 -06:00
|
|
|
def render_content(self) -> List[nodes.Node]:
|
2022-08-17 21:17:08 -06:00
|
|
|
tag_paragraph = self.make_labeled_paragraph('Tags')
|
2022-07-27 20:02:08 -06:00
|
|
|
for tag in self.options.get('tags', []):
|
2022-08-17 20:37:03 -06:00
|
|
|
tag_paragraph += [
|
2022-08-06 15:26:33 -06:00
|
|
|
addnodes.pending_xref(tag, nodes.inline(text=tag), **{
|
|
|
|
'reftype': 'ref',
|
|
|
|
'refdomain': 'std',
|
2022-09-21 18:53:51 -06:00
|
|
|
'reftarget': tag + '-tag-index',
|
2022-09-23 09:10:37 -06:00
|
|
|
'refexplicit': True,
|
2022-08-06 15:26:33 -06:00
|
|
|
'refwarn': True,
|
|
|
|
}),
|
2022-07-27 00:17:21 -06:00
|
|
|
nodes.inline(text=' | '),
|
|
|
|
]
|
2022-09-23 11:10:28 -06:00
|
|
|
self.add_index_entry(self.get_name_or_docname(), tag)
|
2022-08-17 20:37:03 -06:00
|
|
|
tag_paragraph.pop()
|
2022-07-27 00:17:21 -06:00
|
|
|
|
2022-08-17 20:37:03 -06:00
|
|
|
ret_nodes = [tag_paragraph]
|
2022-08-09 23:37:24 -06:00
|
|
|
if 'no-command' in self.options:
|
2022-09-23 11:10:28 -06:00
|
|
|
self.add_index_entry(self.get_name_or_docname() + ' (plugin)', 'all')
|
2022-08-25 22:46:23 -06:00
|
|
|
ret_nodes += [make_summary(self.env.app.builder, self.options.get('summary', ''))]
|
2022-08-09 23:37:24 -06:00
|
|
|
return ret_nodes
|
2022-08-06 21:08:51 -06:00
|
|
|
|
2022-08-06 23:03:15 -06:00
|
|
|
def run(self):
|
|
|
|
out = DFHackToolDirectiveBase.run(self)
|
|
|
|
if 'no-command' not in self.options:
|
|
|
|
out += [self.wrap_box(*DFHackCommandDirective.render_content(self))]
|
|
|
|
return out
|
|
|
|
|
2022-08-06 21:08:51 -06:00
|
|
|
|
|
|
|
class DFHackCommandDirective(DFHackToolDirectiveBase):
|
2022-08-09 23:37:24 -06:00
|
|
|
option_spec = {
|
|
|
|
'summary': rst_directives.unchanged_required,
|
|
|
|
}
|
|
|
|
|
2022-08-06 23:03:15 -06:00
|
|
|
def render_content(self) -> List[nodes.Node]:
|
2022-08-06 23:37:14 -06:00
|
|
|
command = self.get_name_or_docname()
|
2022-09-23 11:10:28 -06:00
|
|
|
self.add_index_entry(command, 'all')
|
|
|
|
return [
|
|
|
|
self.make_labeled_paragraph('Command', command, content_class=nodes.literal),
|
2022-08-25 22:46:23 -06:00
|
|
|
make_summary(self.env.app.builder, self.options.get('summary', '')),
|
2022-08-17 21:17:08 -06:00
|
|
|
*render_dfhack_keybind(command, builder=self.env.app.builder),
|
2022-07-27 00:17:21 -06:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-09-21 18:53:51 -06:00
|
|
|
def get_tags():
|
|
|
|
groups = {}
|
|
|
|
group_re = re.compile(r'"([^"]+)"')
|
|
|
|
tag_re = re.compile(r'- `([^`]+)-tag-index`: (.*)')
|
|
|
|
with open('docs/Tags.rst') as f:
|
|
|
|
lines = f.readlines()
|
|
|
|
for line in lines:
|
|
|
|
line = line.strip()
|
|
|
|
m = re.match(group_re, line)
|
|
|
|
if m:
|
|
|
|
group = m.group(1)
|
|
|
|
groups[group] = []
|
|
|
|
continue
|
|
|
|
m = re.match(tag_re, line)
|
|
|
|
if m:
|
|
|
|
tag = m.group(1)
|
|
|
|
desc = m.group(2)
|
|
|
|
groups[group].append((tag, desc))
|
|
|
|
return groups
|
|
|
|
|
|
|
|
|
2022-09-23 09:10:37 -06:00
|
|
|
def tag_domain_get_objects(self):
|
|
|
|
for obj in self.data['objects']:
|
|
|
|
yield(obj)
|
|
|
|
|
|
|
|
def tag_domain_merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
|
2022-09-23 10:32:08 -06:00
|
|
|
seen = set()
|
|
|
|
objs = self.data['objects']
|
|
|
|
for obj in objs:
|
|
|
|
seen.add(obj[0])
|
|
|
|
for obj in otherdata['objects']:
|
|
|
|
if obj[0] not in seen:
|
|
|
|
objs.append(obj)
|
|
|
|
objs.sort()
|
2022-09-23 09:10:37 -06:00
|
|
|
|
|
|
|
def tag_index_generate(self, docnames: Optional[Iterable[str]] = None) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
|
2022-09-23 10:32:08 -06:00
|
|
|
content = defaultdict(list)
|
|
|
|
for name, desc, _, docname, _, _ in self.domain.data['objects']:
|
|
|
|
first_letter = name[0].lower()
|
|
|
|
content[first_letter].append(
|
|
|
|
IndexEntry(name, 0, docname, '', '', '', desc))
|
|
|
|
return (sorted(content.items()), False)
|
2022-09-21 18:53:51 -06:00
|
|
|
|
2022-09-23 11:10:28 -06:00
|
|
|
def register_index(app, tag, title):
|
|
|
|
domain_class = type(tag+'Domain', (Domain, ), {
|
|
|
|
'name': tag,
|
|
|
|
'label': 'Container domain for tag: ' + tag,
|
|
|
|
'initial_data': {'objects': []},
|
|
|
|
'merge_domaindata': tag_domain_merge_domaindata,
|
|
|
|
'get_objects': tag_domain_get_objects,
|
|
|
|
})
|
|
|
|
index_class = type(tag+'Index', (Index, ), {
|
|
|
|
'name': 'tag-index',
|
|
|
|
'localname': title,
|
|
|
|
'shortname': tag,
|
|
|
|
'generate': tag_index_generate,
|
|
|
|
})
|
|
|
|
app.add_domain(domain_class)
|
|
|
|
app.add_index_to_domain(tag, index_class)
|
2022-09-21 18:53:51 -06:00
|
|
|
|
|
|
|
def init_tag_indices(app):
|
|
|
|
os.makedirs('docs/tags', mode=0o755, exist_ok=True)
|
|
|
|
tag_groups = get_tags()
|
|
|
|
for tag_group in tag_groups:
|
|
|
|
with dfhack.util.write_file_if_changed(('docs/tags/by{group}.rst').format(group=tag_group)) as topidx:
|
|
|
|
for tag_tuple in tag_groups[tag_group]:
|
|
|
|
tag, desc = tag_tuple[0], tag_tuple[1]
|
|
|
|
topidx.write(('- `{name} <{name}-tag-index>`\n').format(name=tag))
|
|
|
|
topidx.write((' {desc}\n').format(desc=desc))
|
2022-09-23 11:21:53 -06:00
|
|
|
register_index(app, tag, '%s<h4>%s</h4>' % (tag, desc))
|
2022-09-21 18:53:51 -06:00
|
|
|
|
|
|
|
|
2022-07-27 00:17:21 -06:00
|
|
|
def register(app):
|
|
|
|
app.add_directive('dfhack-tool', DFHackToolDirective)
|
2022-08-06 21:08:51 -06:00
|
|
|
app.add_directive('dfhack-command', DFHackCommandDirective)
|
2022-08-06 23:37:14 -06:00
|
|
|
_KEYBINDS.update(scan_all_keybinds(os.path.join(dfhack.util.DFHACK_ROOT, 'data', 'init')))
|
|
|
|
|
2022-07-27 00:17:21 -06:00
|
|
|
|
|
|
|
def setup(app):
|
|
|
|
app.connect('builder-inited', register)
|
|
|
|
|
2022-09-23 11:10:28 -06:00
|
|
|
register_index(app, 'all', 'Index of DFHack tools')
|
2022-09-21 18:53:51 -06:00
|
|
|
init_tag_indices(app)
|
|
|
|
|
2022-08-07 00:16:38 -06:00
|
|
|
# TODO: re-enable once detection is corrected
|
|
|
|
# app.connect('build-finished', lambda *_: check_missing_keybinds())
|
|
|
|
|
2022-07-27 00:17:21 -06:00
|
|
|
return {
|
|
|
|
'version': '0.1',
|
2022-09-23 09:10:37 -06:00
|
|
|
'parallel_read_safe': True,
|
|
|
|
'parallel_write_safe': True,
|
2022-07-27 00:17:21 -06:00
|
|
|
}
|