summaryrefslogtreecommitdiffstats
path: root/doc/_exts/rubydomain/rubydomain.py
diff options
context:
space:
mode:
Diffstat (limited to 'doc/_exts/rubydomain/rubydomain.py')
-rw-r--r--doc/_exts/rubydomain/rubydomain.py703
1 files changed, 703 insertions, 0 deletions
diff --git a/doc/_exts/rubydomain/rubydomain.py b/doc/_exts/rubydomain/rubydomain.py
new file mode 100644
index 0000000..db35233
--- /dev/null
+++ b/doc/_exts/rubydomain/rubydomain.py
@@ -0,0 +1,703 @@
+# -*- coding: utf-8 -*-
+"""
+ sphinx.domains.ruby
+ ~~~~~~~~~~~~~~~~~~~
+
+ The Ruby domain.
+
+ :copyright: Copyright 2010 by SHIBUKAWA Yoshiki
+ :license: BSD, see LICENSE for details.
+"""
+
+import re
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive
+
+from sphinx import addnodes
+from sphinx import version_info
+from sphinx.roles import XRefRole
+from sphinx.locale import _
+from sphinx.domains import Domain, ObjType, Index
+from sphinx.directives import ObjectDescription
+from sphinx.util.nodes import make_refnode
+from sphinx.util.docfields import Field, GroupedField, TypedField
+
+# REs for Ruby signatures
+rb_sig_re = re.compile(
+ r'''^ ([\w.]*\.)? # class name(s)
+ (\$?\w+\??!?) \s* # thing name
+ (?: \((.*)\) # optional: arguments
+ (?:\s* -> \s* (.*))? # return annotation
+ )? $ # and nothing more
+ ''', re.VERBOSE)
+
+rb_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ','
+
+separators = {
+ 'method':'#', 'attr_reader':'#', 'attr_writer':'#', 'attr_accessor':'#',
+ 'function':'.', 'classmethod':'.', 'class':'::', 'module':'::',
+ 'global':'', 'const':'::'}
+
+rb_separator = re.compile(r"(?:\w+)?(?:::)?(?:\.)?(?:#)?")
+
+
+def _iteritems(d):
+
+ for k in d:
+ yield k, d[k]
+
+
+def ruby_rsplit(fullname):
+ items = [item for item in rb_separator.findall(fullname)]
+ return ''.join(items[:-2]), items[-1]
+
+
+class RubyObject(ObjectDescription):
+ """
+ Description of a general Ruby object.
+ """
+ option_spec = {
+ 'noindex': directives.flag,
+ 'module': directives.unchanged,
+ }
+
+ doc_field_types = [
+ TypedField('parameter', label=_('Parameters'),
+ names=('param', 'parameter', 'arg', 'argument'),
+ typerolename='obj', typenames=('paramtype', 'type')),
+ TypedField('variable', label=_('Variables'), rolename='obj',
+ names=('var', 'ivar', 'cvar'),
+ typerolename='obj', typenames=('vartype',)),
+ GroupedField('exceptions', label=_('Raises'), rolename='exc',
+ names=('raises', 'raise', 'exception', 'except'),
+ can_collapse=True),
+ Field('returnvalue', label=_('Returns'), has_arg=False,
+ names=('returns', 'return')),
+ Field('returntype', label=_('Return type'), has_arg=False,
+ names=('rtype',)),
+ ]
+
+ def get_signature_prefix(self, sig):
+ """
+ May return a prefix to put before the object name in the signature.
+ """
+ return ''
+
+ def needs_arglist(self):
+ """
+ May return true if an empty argument list is to be generated even if
+ the document contains none.
+ """
+ return False
+
+ def handle_signature(self, sig, signode):
+ """
+ Transform a Ruby signature into RST nodes.
+ Returns (fully qualified name of the thing, classname if any).
+
+ If inside a class, the current class name is handled intelligently:
+ * it is stripped from the displayed name if present
+ * it is added to the full name (return value) if not present
+ """
+ m = rb_sig_re.match(sig)
+ if m is None:
+ raise ValueError
+ name_prefix, name, arglist, retann = m.groups()
+ if not name_prefix:
+ name_prefix = ""
+ # determine module and class name (if applicable), as well as full name
+ modname = self.options.get(
+ 'module', self.env.temp_data.get('rb:module'))
+ classname = self.env.temp_data.get('rb:class')
+ if self.objtype == 'global':
+ add_module = False
+ modname = None
+ classname = None
+ fullname = name
+ elif classname:
+ add_module = False
+ if name_prefix and name_prefix.startswith(classname):
+ fullname = name_prefix + name
+ # class name is given again in the signature
+ name_prefix = name_prefix[len(classname):].lstrip('.')
+ else:
+ separator = separators[self.objtype]
+ fullname = classname + separator + name_prefix + name
+ else:
+ add_module = True
+ if name_prefix:
+ classname = name_prefix.rstrip('.')
+ fullname = name_prefix + name
+ else:
+ classname = ''
+ fullname = name
+
+ signode['module'] = modname
+ signode['class'] = self.class_name = classname
+ signode['fullname'] = fullname
+
+ sig_prefix = self.get_signature_prefix(sig)
+ if sig_prefix:
+ signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
+
+ if name_prefix:
+ signode += addnodes.desc_addname(name_prefix, name_prefix)
+ # exceptions are a special case, since they are documented in the
+ # 'exceptions' module.
+ elif add_module and self.env.config.add_module_names:
+ if self.objtype == 'global':
+ nodetext = ''
+ signode += addnodes.desc_addname(nodetext, nodetext)
+ else:
+ modname = self.options.get(
+ 'module', self.env.temp_data.get('rb:module'))
+ if modname and modname != 'exceptions':
+ nodetext = modname + separators[self.objtype]
+ signode += addnodes.desc_addname(nodetext, nodetext)
+
+ signode += addnodes.desc_name(name, name)
+ if not arglist:
+ if self.needs_arglist():
+ # for callables, add an empty parameter list
+ signode += addnodes.desc_parameterlist()
+ if retann:
+ signode += addnodes.desc_returns(retann, retann)
+ return fullname, name_prefix
+ signode += addnodes.desc_parameterlist()
+
+ stack = [signode[-1]]
+ for token in rb_paramlist_re.split(arglist):
+ if token == '[':
+ opt = addnodes.desc_optional()
+ stack[-1] += opt
+ stack.append(opt)
+ elif token == ']':
+ try:
+ stack.pop()
+ except IndexError:
+ raise ValueError
+ elif not token or token == ',' or token.isspace():
+ pass
+ else:
+ token = token.strip()
+ stack[-1] += addnodes.desc_parameter(token, token)
+ if len(stack) != 1:
+ raise ValueError
+ if retann:
+ signode += addnodes.desc_returns(retann, retann)
+ return fullname, name_prefix
+
+ def get_index_text(self, modname, name):
+ """
+ Return the text for the index entry of the object.
+ """
+ raise NotImplementedError('must be implemented in subclasses')
+
+ def _is_class_member(self):
+ return self.objtype.endswith('method') or self.objtype.startswith('attr')
+
+ def add_target_and_index(self, name_cls, sig, signode):
+ if self.objtype == 'global':
+ modname = ''
+ else:
+ modname = self.options.get(
+ 'module', self.env.temp_data.get('rb:module'))
+ separator = separators[self.objtype]
+ if self._is_class_member():
+ if signode['class']:
+ prefix = modname and modname + '::' or ''
+ else:
+ prefix = modname and modname + separator or ''
+ else:
+ prefix = modname and modname + separator or ''
+ fullname = prefix + name_cls[0]
+ # note target
+ if fullname not in self.state.document.ids:
+ signode['names'].append(fullname)
+ signode['ids'].append(fullname)
+ signode['first'] = (not self.names)
+ self.state.document.note_explicit_target(signode)
+ objects = self.env.domaindata['rb']['objects']
+ if fullname in objects:
+ self.env.warn(
+ self.env.docname,
+ 'duplicate object description of %s, ' % fullname +
+ 'other instance in ' +
+ self.env.doc2path(objects[fullname][0]),
+ self.lineno)
+ objects[fullname] = (self.env.docname, self.objtype)
+
+ indextext = self.get_index_text(modname, name_cls)
+ if indextext:
+ self.indexnode['entries'].append(
+ _make_index('single', indextext, fullname, fullname))
+
+ def before_content(self):
+ # needed for automatic qualification of members (reset in subclasses)
+ self.clsname_set = False
+
+ def after_content(self):
+ if self.clsname_set:
+ self.env.temp_data['rb:class'] = None
+
+
+class RubyModulelevel(RubyObject):
+ """
+ Description of an object on module level (functions, data).
+ """
+
+ def needs_arglist(self):
+ return self.objtype == 'function'
+
+ def get_index_text(self, modname, name_cls):
+ if self.objtype == 'function':
+ if not modname:
+ return _('%s() (global function)') % name_cls[0]
+ return _('%s() (module function in %s)') % (name_cls[0], modname)
+ else:
+ return ''
+
+
+class RubyGloballevel(RubyObject):
+ """
+ Description of an object on module level (functions, data).
+ """
+
+ def get_index_text(self, modname, name_cls):
+ if self.objtype == 'global':
+ return _('%s (global variable)') % name_cls[0]
+ else:
+ return ''
+
+
+class RubyEverywhere(RubyObject):
+ """
+ Description of a class member (methods, attributes).
+ """
+
+ def needs_arglist(self):
+ return self.objtype == 'method'
+
+ def get_index_text(self, modname, name_cls):
+ name, cls = name_cls
+ add_modules = self.env.config.add_module_names
+ if self.objtype == 'method':
+ try:
+ clsname, methname = ruby_rsplit(name)
+ except ValueError:
+ if modname:
+ return _('%s() (in module %s)') % (name, modname)
+ else:
+ return '%s()' % name
+ if modname and add_modules:
+ return _('%s() (%s::%s method)') % (methname, modname,
+ clsname)
+ else:
+ return _('%s() (%s method)') % (methname, clsname)
+ else:
+ return ''
+
+
+class RubyClasslike(RubyObject):
+ """
+ Description of a class-like object (classes, exceptions).
+ """
+
+ def get_signature_prefix(self, sig):
+ return self.objtype + ' '
+
+ def get_index_text(self, modname, name_cls):
+ if self.objtype == 'class':
+ if not modname:
+ return _('%s (class)') % name_cls[0]
+ return _('%s (class in %s)') % (name_cls[0], modname)
+ elif self.objtype == 'exception':
+ return name_cls[0]
+ else:
+ return ''
+
+ def before_content(self):
+ RubyObject.before_content(self)
+ if self.names:
+ self.env.temp_data['rb:class'] = self.names[0][0]
+ self.clsname_set = True
+
+
+class RubyClassmember(RubyObject):
+ """
+ Description of a class member (methods, attributes).
+ """
+
+ def needs_arglist(self):
+ return self.objtype.endswith('method')
+
+ def get_signature_prefix(self, sig):
+ if self.objtype == 'classmethod':
+ return "classmethod %s." % self.class_name
+ elif self.objtype == 'attr_reader':
+ return "attribute [R] "
+ elif self.objtype == 'attr_writer':
+ return "attribute [W] "
+ elif self.objtype == 'attr_accessor':
+ return "attribute [R/W] "
+ return ''
+
+ def get_index_text(self, modname, name_cls):
+ name, cls = name_cls
+ add_modules = self.env.config.add_module_names
+ if self.objtype == 'classmethod':
+ try:
+ clsname, methname = ruby_rsplit(name)
+ except ValueError:
+ return '%s()' % name
+ if modname:
+ return _('%s() (%s.%s class method)') % (methname, modname,
+ clsname)
+ else:
+ return _('%s() (%s class method)') % (methname, clsname)
+ elif self.objtype.startswith('attr'):
+ try:
+ clsname, attrname = ruby_rsplit(name)
+ except ValueError:
+ return name
+ if modname and add_modules:
+ return _('%s (%s.%s attribute)') % (attrname, modname, clsname)
+ else:
+ return _('%s (%s attribute)') % (attrname, clsname)
+ else:
+ return ''
+
+ def before_content(self):
+ RubyObject.before_content(self)
+ lastname = self.names and self.names[-1][1]
+ if lastname and not self.env.temp_data.get('rb:class'):
+ self.env.temp_data['rb:class'] = lastname.strip('.')
+ self.clsname_set = True
+
+
+class RubyModule(Directive):
+ """
+ Directive to mark description of a new module.
+ """
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {
+ 'platform': lambda x: x,
+ 'synopsis': lambda x: x,
+ 'noindex': directives.flag,
+ 'deprecated': directives.flag,
+ }
+
+ def run(self):
+ env = self.state.document.settings.env
+ modname = self.arguments[0].strip()
+ noindex = 'noindex' in self.options
+ env.temp_data['rb:module'] = modname
+ env.domaindata['rb']['modules'][modname] = \
+ (env.docname, self.options.get('synopsis', ''),
+ self.options.get('platform', ''), 'deprecated' in self.options)
+ targetnode = nodes.target('', '', ids=['module-' + modname], ismod=True)
+ self.state.document.note_explicit_target(targetnode)
+ ret = [targetnode]
+ # XXX this behavior of the module directive is a mess...
+ if 'platform' in self.options:
+ platform = self.options['platform']
+ node = nodes.paragraph()
+ node += nodes.emphasis('', _('Platforms: '))
+ node += nodes.Text(platform, platform)
+ ret.append(node)
+ # the synopsis isn't printed; in fact, it is only used in the
+ # modindex currently
+ if not noindex:
+ indextext = _('%s (module)') % modname
+ inode = addnodes.index(entries=[_make_index(
+ 'single', indextext, 'module-' + modname, modname)])
+ ret.append(inode)
+ return ret
+
+def _make_index(entrytype, entryname, target, ignored, key=None):
+ # Sphinx 1.4 introduced backward incompatible changes, it now
+ # requires 5 tuples. Last one is categorization key. See
+ # http://www.sphinx-doc.org/en/stable/extdev/nodes.html#sphinx.addnodes.index
+ if version_info >= (1, 4, 0, '', 0):
+ return (entrytype, entryname, target, ignored, key)
+ else:
+ return (entrytype, entryname, target, ignored)
+
+class RubyCurrentModule(Directive):
+ """
+ This directive is just to tell Sphinx that we're documenting
+ stuff in module foo, but links to module foo won't lead here.
+ """
+
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def run(self):
+ env = self.state.document.settings.env
+ modname = self.arguments[0].strip()
+ if modname == 'None':
+ env.temp_data['rb:module'] = None
+ else:
+ env.temp_data['rb:module'] = modname
+ return []
+
+
+class RubyXRefRole(XRefRole):
+ def process_link(self, env, refnode, has_explicit_title, title, target):
+ if not has_explicit_title:
+ title = title.lstrip('.') # only has a meaning for the target
+ title = title.lstrip('#')
+ if title.startswith("::"):
+ title = title[2:]
+ target = target.lstrip('~') # only has a meaning for the title
+ # if the first character is a tilde, don't display the module/class
+ # parts of the contents
+ if title[0:1] == '~':
+ m = re.search(r"(?:\.)?(?:#)?(?:::)?(.*)\Z", title)
+ if m:
+ title = m.group(1)
+ if not title.startswith("$"):
+ refnode['rb:module'] = env.temp_data.get('rb:module')
+ refnode['rb:class'] = env.temp_data.get('rb:class')
+ # if the first character is a dot, search more specific namespaces first
+ # else search builtins first
+ if target[0:1] == '.':
+ target = target[1:]
+ refnode['refspecific'] = True
+ return title, target
+
+
+class RubyModuleIndex(Index):
+ """
+ Index subclass to provide the Ruby module index.
+ """
+
+ name = 'modindex'
+ localname = _('Ruby Module Index')
+ shortname = _('modules')
+
+ def generate(self, docnames=None):
+ content = {}
+ # list of prefixes to ignore
+ ignores = self.domain.env.config['modindex_common_prefix']
+ ignores = sorted(ignores, key=len, reverse=True)
+ # list of all modules, sorted by module name
+ modules = sorted(_iteritems(self.domain.data['modules']),
+ key=lambda x: x[0].lower())
+ # sort out collapsible modules
+ prev_modname = ''
+ num_toplevels = 0
+ for modname, (docname, synopsis, platforms, deprecated) in modules:
+ if docnames and docname not in docnames:
+ continue
+
+ for ignore in ignores:
+ if modname.startswith(ignore):
+ modname = modname[len(ignore):]
+ stripped = ignore
+ break
+ else:
+ stripped = ''
+
+ # we stripped the whole module name?
+ if not modname:
+ modname, stripped = stripped, ''
+
+ entries = content.setdefault(modname[0].lower(), [])
+
+ package = modname.split('::')[0]
+ if package != modname:
+ # it's a submodule
+ if prev_modname == package:
+ # first submodule - make parent a group head
+ entries[-1][1] = 1
+ elif not prev_modname.startswith(package):
+ # submodule without parent in list, add dummy entry
+ entries.append([stripped + package, 1, '', '', '', '', ''])
+ subtype = 2
+ else:
+ num_toplevels += 1
+ subtype = 0
+
+ qualifier = deprecated and _('Deprecated') or ''
+ entries.append([stripped + modname, subtype, docname,
+ 'module-' + stripped + modname, platforms,
+ qualifier, synopsis])
+ prev_modname = modname
+
+ # apply heuristics when to collapse modindex at page load:
+ # only collapse if number of toplevel modules is larger than
+ # number of submodules
+ collapse = len(modules) - num_toplevels < num_toplevels
+
+ # sort by first letter
+ content = sorted(_iteritems(content))
+
+ return content, collapse
+
+
+class RubyDomain(Domain):
+ """Ruby language domain."""
+ name = 'rb'
+ label = 'Ruby'
+ object_types = {
+ 'function': ObjType(_('function'), 'func', 'obj'),
+ 'global': ObjType(_('global variable'), 'global', 'obj'),
+ 'method': ObjType(_('method'), 'meth', 'obj'),
+ 'class': ObjType(_('class'), 'class', 'obj'),
+ 'exception': ObjType(_('exception'), 'exc', 'obj'),
+ 'classmethod': ObjType(_('class method'), 'meth', 'obj'),
+ 'attr_reader': ObjType(_('attribute'), 'attr', 'obj'),
+ 'attr_writer': ObjType(_('attribute'), 'attr', 'obj'),
+ 'attr_accessor': ObjType(_('attribute'), 'attr', 'obj'),
+ 'const': ObjType(_('const'), 'const', 'obj'),
+ 'module': ObjType(_('module'), 'mod', 'obj'),
+ }
+
+ directives = {
+ 'function': RubyModulelevel,
+ 'global': RubyGloballevel,
+ 'method': RubyEverywhere,
+ 'const': RubyEverywhere,
+ 'class': RubyClasslike,
+ 'exception': RubyClasslike,
+ 'classmethod': RubyClassmember,
+ 'attr_reader': RubyClassmember,
+ 'attr_writer': RubyClassmember,
+ 'attr_accessor': RubyClassmember,
+ 'module': RubyModule,
+ 'currentmodule': RubyCurrentModule,
+ }
+
+ roles = {
+ 'func': RubyXRefRole(fix_parens=False),
+ 'global':RubyXRefRole(),
+ 'class': RubyXRefRole(),
+ 'exc': RubyXRefRole(),
+ 'meth': RubyXRefRole(fix_parens=False),
+ 'attr': RubyXRefRole(),
+ 'const': RubyXRefRole(),
+ 'mod': RubyXRefRole(),
+ 'obj': RubyXRefRole(),
+ }
+ initial_data = {
+ 'objects': {}, # fullname -> docname, objtype
+ 'modules': {}, # modname -> docname, synopsis, platform, deprecated
+ }
+ indices = [
+ RubyModuleIndex,
+ ]
+
+ def clear_doc(self, docname):
+ for fullname, (fn, _) in list(self.data['objects'].items()):
+ if fn == docname:
+ del self.data['objects'][fullname]
+ for modname, (fn, _, _, _) in list(self.data['modules'].items()):
+ if fn == docname:
+ del self.data['modules'][modname]
+
+ def find_obj(self, env, modname, classname, name, type, searchorder=0):
+ """
+ Find a Ruby object for "name", perhaps using the given module and/or
+ classname.
+ """
+ # skip parens
+ if name[-2:] == '()':
+ name = name[:-2]
+
+ if not name:
+ return None, None
+
+ objects = self.data['objects']
+
+ newname = None
+ if searchorder == 1:
+ if modname and classname and \
+ modname + '::' + classname + '#' + name in objects:
+ newname = modname + '::' + classname + '#' + name
+ elif modname and classname and \
+ modname + '::' + classname + '.' + name in objects:
+ newname = modname + '::' + classname + '.' + name
+ elif modname and modname + '::' + name in objects:
+ newname = modname + '::' + name
+ elif modname and modname + '#' + name in objects:
+ newname = modname + '#' + name
+ elif modname and modname + '.' + name in objects:
+ newname = modname + '.' + name
+ elif classname and classname + '.' + name in objects:
+ newname = classname + '.' + name
+ elif classname and classname + '#' + name in objects:
+ newname = classname + '#' + name
+ elif name in objects:
+ newname = name
+ else:
+ if name in objects:
+ newname = name
+ elif classname and classname + '.' + name in objects:
+ newname = classname + '.' + name
+ elif classname and classname + '#' + name in objects:
+ newname = classname + '#' + name
+ elif modname and modname + '::' + name in objects:
+ newname = modname + '::' + name
+ elif modname and modname + '#' + name in objects:
+ newname = modname + '#' + name
+ elif modname and modname + '.' + name in objects:
+ newname = modname + '.' + name
+ elif modname and classname and \
+ modname + '::' + classname + '#' + name in objects:
+ newname = modname + '::' + classname + '#' + name
+ elif modname and classname and \
+ modname + '::' + classname + '.' + name in objects:
+ newname = modname + '::' + classname + '.' + name
+ # special case: object methods
+ elif type in ('func', 'meth') and '.' not in name and \
+ 'object.' + name in objects:
+ newname = 'object.' + name
+ if newname is None:
+ return None, None
+ return newname, objects[newname]
+
+ def resolve_xref(self, env, fromdocname, builder,
+ typ, target, node, contnode):
+ if (typ == 'mod' or
+ typ == 'obj' and target in self.data['modules']):
+ docname, synopsis, platform, deprecated = \
+ self.data['modules'].get(target, ('','','', ''))
+ if not docname:
+ return None
+ else:
+ title = '%s%s%s' % ((platform and '(%s) ' % platform),
+ synopsis,
+ (deprecated and ' (deprecated)' or ''))
+ return make_refnode(builder, fromdocname, docname,
+ 'module-' + target, contnode, title)
+ else:
+ modname = node.get('rb:module')
+ clsname = node.get('rb:class')
+ searchorder = node.hasattr('refspecific') and 1 or 0
+ name, obj = self.find_obj(env, modname, clsname,
+ target, typ, searchorder)
+ if not obj:
+ return None
+ else:
+ return make_refnode(builder, fromdocname, obj[0], name,
+ contnode, name)
+
+ def get_objects(self):
+ for modname, info in _iteritems(self.data['modules']):
+ yield (modname, modname, 'module', info[0], 'module-' + modname, 0)
+ for refname, (docname, type) in _iteritems(self.data['objects']):
+ yield (refname, refname, type, docname, refname, 1)
+
+
+def setup(app):
+ app.add_domain(RubyDomain)