| # -*- 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 l_, _ |
| 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=l_('Parameters'), |
| names=('param', 'parameter', 'arg', 'argument'), |
| typerolename='obj', typenames=('paramtype', 'type')), |
| TypedField('variable', label=l_('Variables'), rolename='obj', |
| names=('var', 'ivar', 'cvar'), |
| typerolename='obj', typenames=('vartype',)), |
| GroupedField('exceptions', label=l_('Raises'), rolename='exc', |
| names=('raises', 'raise', 'exception', 'except'), |
| can_collapse=True), |
| Field('returnvalue', label=l_('Returns'), has_arg=False, |
| names=('returns', 'return')), |
| Field('returntype', label=l_('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 = l_('Ruby Module Index') |
| shortname = l_('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 collapsable 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(l_('function'), 'func', 'obj'), |
| 'global': ObjType(l_('global variable'), 'global', 'obj'), |
| 'method': ObjType(l_('method'), 'meth', 'obj'), |
| 'class': ObjType(l_('class'), 'class', 'obj'), |
| 'exception': ObjType(l_('exception'), 'exc', 'obj'), |
| 'classmethod': ObjType(l_('class method'), 'meth', 'obj'), |
| 'attr_reader': ObjType(l_('attribute'), 'attr', 'obj'), |
| 'attr_writer': ObjType(l_('attribute'), 'attr', 'obj'), |
| 'attr_accessor': ObjType(l_('attribute'), 'attr', 'obj'), |
| 'const': ObjType(l_('const'), 'const', 'obj'), |
| 'module': ObjType(l_('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) |