emacs-ipython-notebook/doc/eldomain/eldomain.py

458 lines
15 KiB
Python

# eldomain is a Emacs Lisp domain for the Sphinx documentation tool.
# Copyright (C) 2012 Takafumi Arakaki
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The Emacs Lisp domain
~~~~~~~~~~~~~~~~~~~~~
This Sphinx extension provides directives and roles under the domain
``el`` for documenting Emacs Lisp program. It also provides autodoc
functionality -- docstrings in the Emacs Lisp is automatically
inserted in the documentation.
.. note::
The source code is heavily borrowed from the `Common Lisp domain`_.
.. _`Common Lisp domain`:
https://github.com/russell/sphinxcontrib-cldomain
"""
import os
from os import path
import re
import itertools
import subprocess
import json
from docutils import nodes
from docutils.statemachine import string2lines, StringList
from sphinx import addnodes
from sphinx.locale import l_, _
from sphinx.roles import XRefRole
from sphinx.domains import Domain, ObjType
from sphinx.directives import ObjectDescription
from sphinx.util.nodes import make_refnode
from sphinx.util.compat import Directive
from sphinx.util.docfields import Field, GroupedField
DATA = {}
DATA_DOC_STRINGS = {}
DATA_ARGS = {}
def bool_option(arg):
"""Used to convert flag options to auto directives. (Instead of
directives.flag(), which returns None).
"""
return True
def string_list(delimiter):
"""
Return a parser for option_spec to parse strings separated by `delimiter`.
>>> parser = string_list(',')
>>> parser('a, b, c')
['a', 'b', 'c']
"""
return lambda argument: [v.strip() for v in argument.split(delimiter)]
class ELSExp(ObjectDescription):
doc_field_types = [
GroupedField('parameter', label=l_('Parameters'),
names=('param', 'parameter', 'arg', 'argument',
'keyword', 'kwparam')),
Field('returnvalue', label=l_('Returns'), has_arg=False,
names=('returns', 'return')),
]
option_spec = {
'nodoc': bool_option, 'noindex': bool_option,
}
def handle_signature(self, sig, signode):
symbol_name = []
def render_sexp(sexp, signode=None, prepend_node=None):
desc_sexplist = addnodes.desc_parameterlist()
desc_sexplist.child_text_separator = ' '
if prepend_node:
desc_sexplist.append(prepend_node)
if signode:
signode.append(desc_sexplist)
for atom in sexp:
if isinstance(atom, list):
render_sexp(atom, desc_sexplist)
else:
render_atom(atom, desc_sexplist)
return desc_sexplist
def render_atom(token, signode, noemph=True):
"add syntax hi-lighting to interesting atoms"
if token.startswith("&") or token.startswith(":"):
signode.append(addnodes.desc_parameter(token, token))
else:
signode.append(addnodes.desc_parameter(token, token))
package = self.env.temp_data.get('el:package')
objtype = self.get_signature_prefix(sig)
signode.append(addnodes.desc_annotation(objtype, objtype))
lisp_args = DATA_ARGS.get(package, {}).get(sig, [])
if lisp_args:
function_name = addnodes.desc_name(sig, sig + " ")
else:
function_name = addnodes.desc_name(sig, sig)
if lisp_args:
arg_list = render_sexp(lisp_args, prepend_node=function_name)
signode.append(arg_list)
else:
signode.append(function_name)
symbol_name = sig
if not symbol_name:
raise Exception("Unknown symbol type for signature %s" % sig)
return objtype.strip(), symbol_name
def get_index_text(self, name, type):
return _('%s (Lisp %s)') % (name, type)
def get_signature_prefix(self, sig):
return self.objtype + ' '
def add_target_and_index(self, name, sig, signode):
# note target
type, name = name
if name not in self.state.document.ids:
signode['names'].append(name)
signode['ids'].append(name)
signode['first'] = (not self.names)
self.state.document.note_explicit_target(signode)
inv = self.env.domaindata['el']['symbols']
if name in inv:
self.state_machine.reporter.warning(
'duplicate symbol description of %s, ' % name +
'other instance in ' + self.env.doc2path(inv[name][0]),
line=self.lineno)
inv[name] = (self.env.docname, self.objtype)
indextext = self.get_index_text(name, type)
if indextext:
self.indexnode['entries'].append(('single', indextext, name, ''))
def run(self):
result = super(ELSExp, self).run()
if "nodoc" not in self.options:
package = self.env.temp_data.get('el:package')
node = addnodes.desc_content()
string = DATA_DOC_STRINGS.get(package, {}) \
.get(self.names[0][1], "")
lines = string2lines(string)
self.state.nested_parse(StringList(lines), 0, node)
if (result[1][1].children and
isinstance(result[1][1][0], nodes.field_list)):
cresult = result[1][1].deepcopy()
target = result[1][1]
target.clear()
target.append(cresult[0])
target.extend(node)
target.extend(cresult[1:])
else:
cresult = result[1][1].deepcopy()
target = result[1][1]
target.clear()
target.extend(node)
target.extend(cresult)
return result
class ELCurrentPackage(Directive):
"""
This directive is just to tell Sphinx that we're documenting stuff in
namespace foo.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {}
def run(self):
env = self.state.document.settings.env
env.temp_data['el:package'] = self.arguments[0]
return []
def parse_text_list(argument, delimiter=','):
"""
Converts a space- or comma-separated list of values into a list
"""
if delimiter in argument:
entries = argument.split(delimiter)
else:
entries = argument.split()
return [v.strip() for v in entries]
def compose(f, g):
def h(*args, **kwds):
return f(g(*args, **kwds))
return h
def filter_by_exclude_regexp_list(candidates, regexp_list, getter=lambda x: x):
"""
Exclude elements in `candidates` that matches to one of the regexp.
>>> filter_by_exclude_regexp_list(['a', 'aa', 'bb'], ['a+'])
['bb']
>>> filter_by_exclude_regexp_list(['a', 'ab', 'bb'], ['a+', 'a?b$'])
['bb']
>>> filter_by_exclude_regexp_list(
... [{'key': 'a'}, {'key': 'ab'}, {'key': 'bb'}],
... ['a+', 'a?b$'],
... lambda x: x['key'])
[{'key': 'bb'}]
"""
for compiled in map(re.compile, regexp_list):
test = compose(compiled.match, getter)
candidates = itertools.filterfalse(test, candidates)
return list(candidates)
def simple_sed(scripts, string):
r"""
A simple sed-like function
>>> simple_sed(['s/before/after/g'], 'and then before that ...')
'and then after that ...'
>>> simple_sed([r's/([0-9])/\1\1/g'], 'there are 2 apples')
'there are 22 apples'
"""
for scr in scripts:
scr = scr.lstrip('s')
scr_split = scr[1:].split(scr[0])
(regexp, replace) = scr_split[:2]
string = re.sub(regexp, replace, string)
return string
class ELKeyMap(Directive):
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {
'exclude': parse_text_list,
'replace': string_list('\n'),
}
def run(self):
env = self.state.document.settings.env
package = env.temp_data.get('el:package')
keymap_list = DATA.get(package, {}).get('keymap', [])
keymap_name = self.arguments[0]
for keymap in keymap_list:
if keymap['name'] == keymap_name:
break
else:
return [self.state.reporter.warning(
"Keymap {0} not found".format(keymap_name))]
nodelist = []
mapdoc = keymap['doc']
if mapdoc:
nd = nodes.paragraph()
lines = string2lines(doc_to_rst(mapdoc))
self.state.nested_parse(StringList(lines), 0, nd)
nodelist.append(nd)
exclude = self.options.get('exclude', [])
replace = self.options.get('replace', [])
for keybind in filter_by_exclude_regexp_list(
keymap['data'], exclude, lambda x: x['func']):
desc = addnodes.desc()
desc['domain'] = 'el'
desc['objtype'] = 'keybind'
desc['noindex'] = False
signode = addnodes.desc_signature()
# signode += addnodes.desc_annotation("", 'keybind ')
key = simple_sed(replace, keybind['key'])
signode += addnodes.desc_name("", key)
signode += addnodes.desc_addname("", " " + keybind['func'])
desc += signode
if keybind['doc']:
nd = addnodes.desc_content()
lines = string2lines(doc_to_rst(keybind['doc']))
self.state.nested_parse(StringList(lines), 0, nd)
desc += nodes.definition("", nd)
nodelist.append(desc)
return nodelist
class ELXRefRole(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target):
if not has_explicit_title:
target = target.lstrip('~') # only has a meaning for the title
# if the first character is a tilde, don't display the package
if title[0:1] == '~':
title = title[1:]
dot = title.rfind(':')
if dot != -1:
title = title[dot + 1:]
return title, target
class ELDomain(Domain):
"""EL language domain."""
name = 'el'
label = 'Common Lisp'
object_types = {
'package': ObjType(l_('package'), 'package'),
'function': ObjType(l_('function'), 'function'),
'macro': ObjType(l_('macro'), 'macro'),
'variable': ObjType(l_('variable'), 'variable'),
}
directives = {
'package': ELCurrentPackage,
'function': ELSExp,
'macro': ELSExp,
'variable': ELSExp,
'keymap': ELKeyMap,
}
roles = {
'symbol': ELXRefRole(),
}
initial_data = {
'symbols': {},
}
def clear_doc(self, docname):
for fullname, (fn, _) in self.data['symbols'].items():
if fn == docname:
del self.data['symbols'][fullname]
def find_obj(self, env, name):
"""Find a Lisp symbol for "name", perhaps using the given package
Returns a list of (name, object entry) tuples.
"""
symbols = self.data['symbols']
if ":" in name:
if name in symbols:
return [(name, symbols[name])]
else:
def filter_symbols(symbol):
symbol = symbol[0]
if name == symbol:
return True
if ":" in symbol:
symbol = symbol.split(":")[1]
if name == symbol:
return True
return False
return [f for f in filter(filter_symbols, symbols.items())]
def resolve_xref(self, env, fromdocname, builder,
typ, target, node, contnode):
matches = self.find_obj(env, target)
if not matches:
return None
elif len(matches) > 1:
env.warn_node(
'more than one target found for cross-reference '
'%r: %s' % (target, ', '.join(match[0] for match in matches)),
node)
name, obj = matches[0]
return make_refnode(builder, fromdocname, obj[0], name,
contnode, name)
def get_symbols(self):
for refname, (docname, type) in self.data['symbols'].items():
yield (refname, refname, type, docname, refname, 1)
def doc_to_rst(docstring):
docstring = _eldoc_quote_re.sub(r":el:symbol:`\1`", docstring)
return docstring
_eldoc_quote_re = re.compile(r"`(\S+)'")
def index_package(emacs, package, prefix, pre_load, extra_args=[]):
"""Call an external lisp program that will return a dictionary of
doc strings for all public symbols."""
lisp_script = path.join(path.dirname(path.realpath(__file__)),
"eldomain.el")
command = [emacs, "-Q", "-batch", "-l", pre_load,
"--eval", '(setq eldomain-prefix "{0}")'.format(prefix),
"-l", lisp_script] + extra_args
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = proc.communicate()
if proc.poll() != 0:
raise RuntimeError(
"Error while executing '{0}'.\n\n"
"STDOUT:\n{1}\n\nSTDERR:\n{2}\n".format(
' '.join(command), stdout, stderr))
DATA[package] = lisp_data = json.loads(stdout.decode())
DATA_DOC_STRINGS[package] = {}
# FIXME: support using same name for function/variable/face
for key in ['face', 'variable', 'function']:
for data in lisp_data[key]:
doc = data['doc']
if doc:
DATA_DOC_STRINGS[package][data['name']] = doc_to_rst(doc)
DATA_ARGS[package] = {}
for data in lisp_data['function']:
DATA_ARGS[package][data['name']] = data['arg']
def load_packages(app):
emacs = app.config.emacs_executable
# `app.confdir` will be ignored if `elisp_pre_load` is an absolute path
pre_load = path.join(app.confdir, app.config.elisp_pre_load)
for (name, prefix) in app.config.elisp_packages.items():
index_package(emacs, name, prefix, pre_load)
def setup(app):
app.add_domain(ELDomain)
app.add_config_value('emacs_executable',
os.getenv("EMACS") or 'emacs',
'env')
app.add_config_value('elisp_pre_load', 'conf.el', 'env')
app.add_config_value('elisp_packages', {}, 'env')
app.connect('builder-inited', load_packages)