• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2"""
3    sphinx.domains.ruby
4    ~~~~~~~~~~~~~~~~~~~
5
6    The Ruby domain.
7
8    :copyright: Copyright 2010 by SHIBUKAWA Yoshiki
9    :license: BSD, see LICENSE for details.
10"""
11
12import re
13
14from docutils import nodes
15from docutils.parsers.rst import directives
16from docutils.parsers.rst import Directive
17
18from sphinx import addnodes
19from sphinx import version_info
20from sphinx.roles import XRefRole
21from sphinx.locale import _
22from sphinx.domains import Domain, ObjType, Index
23from sphinx.directives import ObjectDescription
24from sphinx.util.nodes import make_refnode
25from sphinx.util.docfields import Field, GroupedField, TypedField
26
27# REs for Ruby signatures
28rb_sig_re = re.compile(
29    r'''^ ([\w.]*\.)?            # class name(s)
30          (\$?\w+\??!?)  \s*     # thing name
31          (?: \((.*)\)           # optional: arguments
32           (?:\s* -> \s* (.*))?  #           return annotation
33          )? $                   # and nothing more
34          ''', re.VERBOSE)
35
36rb_paramlist_re = re.compile(r'([\[\],])')  # split at '[', ']' and ','
37
38separators = {
39  'method':'#', 'attr_reader':'#', 'attr_writer':'#', 'attr_accessor':'#',
40  'function':'.', 'classmethod':'.', 'class':'::', 'module':'::',
41  'global':'', 'const':'::'}
42
43rb_separator = re.compile(r"(?:\w+)?(?:::)?(?:\.)?(?:#)?")
44
45
46def _iteritems(d):
47
48    for k in d:
49        yield k, d[k]
50
51
52def ruby_rsplit(fullname):
53    items = [item for item in rb_separator.findall(fullname)]
54    return ''.join(items[:-2]), items[-1]
55
56
57class RubyObject(ObjectDescription):
58    """
59    Description of a general Ruby object.
60    """
61    option_spec = {
62        'noindex': directives.flag,
63        'module': directives.unchanged,
64    }
65
66    doc_field_types = [
67        TypedField('parameter', label=_('Parameters'),
68                   names=('param', 'parameter', 'arg', 'argument'),
69                   typerolename='obj', typenames=('paramtype', 'type')),
70        TypedField('variable', label=_('Variables'), rolename='obj',
71                   names=('var', 'ivar', 'cvar'),
72                   typerolename='obj', typenames=('vartype',)),
73        GroupedField('exceptions', label=_('Raises'), rolename='exc',
74                     names=('raises', 'raise', 'exception', 'except'),
75                     can_collapse=True),
76        Field('returnvalue', label=_('Returns'), has_arg=False,
77              names=('returns', 'return')),
78        Field('returntype', label=_('Return type'), has_arg=False,
79              names=('rtype',)),
80    ]
81
82    def get_signature_prefix(self, sig):
83        """
84        May return a prefix to put before the object name in the signature.
85        """
86        return ''
87
88    def needs_arglist(self):
89        """
90        May return true if an empty argument list is to be generated even if
91        the document contains none.
92        """
93        return False
94
95    def handle_signature(self, sig, signode):
96        """
97        Transform a Ruby signature into RST nodes.
98        Returns (fully qualified name of the thing, classname if any).
99
100        If inside a class, the current class name is handled intelligently:
101        * it is stripped from the displayed name if present
102        * it is added to the full name (return value) if not present
103        """
104        m = rb_sig_re.match(sig)
105        if m is None:
106            raise ValueError
107        name_prefix, name, arglist, retann = m.groups()
108        if not name_prefix:
109            name_prefix = ""
110        # determine module and class name (if applicable), as well as full name
111        modname = self.options.get(
112            'module', self.env.temp_data.get('rb:module'))
113        classname = self.env.temp_data.get('rb:class')
114        if self.objtype == 'global':
115            add_module = False
116            modname = None
117            classname = None
118            fullname = name
119        elif classname:
120            add_module = False
121            if name_prefix and name_prefix.startswith(classname):
122                fullname = name_prefix + name
123                # class name is given again in the signature
124                name_prefix = name_prefix[len(classname):].lstrip('.')
125            else:
126                separator = separators[self.objtype]
127                fullname = classname + separator + name_prefix + name
128        else:
129            add_module = True
130            if name_prefix:
131                classname = name_prefix.rstrip('.')
132                fullname = name_prefix + name
133            else:
134                classname = ''
135                fullname = name
136
137        signode['module'] = modname
138        signode['class'] = self.class_name = classname
139        signode['fullname'] = fullname
140
141        sig_prefix = self.get_signature_prefix(sig)
142        if sig_prefix:
143            signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
144
145        if name_prefix:
146            signode += addnodes.desc_addname(name_prefix, name_prefix)
147        # exceptions are a special case, since they are documented in the
148        # 'exceptions' module.
149        elif add_module and self.env.config.add_module_names:
150            if self.objtype == 'global':
151                nodetext = ''
152                signode += addnodes.desc_addname(nodetext, nodetext)
153            else:
154                modname = self.options.get(
155                    'module', self.env.temp_data.get('rb:module'))
156                if modname and modname != 'exceptions':
157                    nodetext = modname + separators[self.objtype]
158                    signode += addnodes.desc_addname(nodetext, nodetext)
159
160        signode += addnodes.desc_name(name, name)
161        if not arglist:
162            if self.needs_arglist():
163                # for callables, add an empty parameter list
164                signode += addnodes.desc_parameterlist()
165            if retann:
166                signode += addnodes.desc_returns(retann, retann)
167            return fullname, name_prefix
168        signode += addnodes.desc_parameterlist()
169
170        stack = [signode[-1]]
171        for token in rb_paramlist_re.split(arglist):
172            if token == '[':
173                opt = addnodes.desc_optional()
174                stack[-1] += opt
175                stack.append(opt)
176            elif token == ']':
177                try:
178                    stack.pop()
179                except IndexError:
180                    raise ValueError
181            elif not token or token == ',' or token.isspace():
182                pass
183            else:
184                token = token.strip()
185                stack[-1] += addnodes.desc_parameter(token, token)
186        if len(stack) != 1:
187            raise ValueError
188        if retann:
189            signode += addnodes.desc_returns(retann, retann)
190        return fullname, name_prefix
191
192    def get_index_text(self, modname, name):
193        """
194        Return the text for the index entry of the object.
195        """
196        raise NotImplementedError('must be implemented in subclasses')
197
198    def _is_class_member(self):
199        return self.objtype.endswith('method') or self.objtype.startswith('attr')
200
201    def add_target_and_index(self, name_cls, sig, signode):
202        if self.objtype == 'global':
203            modname = ''
204        else:
205            modname = self.options.get(
206                'module', self.env.temp_data.get('rb:module'))
207        separator = separators[self.objtype]
208        if self._is_class_member():
209            if signode['class']:
210                prefix = modname and modname + '::' or ''
211            else:
212                prefix = modname and modname + separator or ''
213        else:
214            prefix = modname and modname + separator or ''
215        fullname = prefix + name_cls[0]
216        # note target
217        if fullname not in self.state.document.ids:
218            signode['names'].append(fullname)
219            signode['ids'].append(fullname)
220            signode['first'] = (not self.names)
221            self.state.document.note_explicit_target(signode)
222            objects = self.env.domaindata['rb']['objects']
223            if fullname in objects:
224                self.env.warn(
225                    self.env.docname,
226                    'duplicate object description of %s, ' % fullname +
227                    'other instance in ' +
228                    self.env.doc2path(objects[fullname][0]),
229                    self.lineno)
230            objects[fullname] = (self.env.docname, self.objtype)
231
232        indextext = self.get_index_text(modname, name_cls)
233        if indextext:
234            self.indexnode['entries'].append(
235                _make_index('single', indextext, fullname, fullname))
236
237    def before_content(self):
238        # needed for automatic qualification of members (reset in subclasses)
239        self.clsname_set = False
240
241    def after_content(self):
242        if self.clsname_set:
243            self.env.temp_data['rb:class'] = None
244
245
246class RubyModulelevel(RubyObject):
247    """
248    Description of an object on module level (functions, data).
249    """
250
251    def needs_arglist(self):
252        return self.objtype == 'function'
253
254    def get_index_text(self, modname, name_cls):
255        if self.objtype == 'function':
256            if not modname:
257                return _('%s() (global function)') % name_cls[0]
258            return _('%s() (module function in %s)') % (name_cls[0], modname)
259        else:
260            return ''
261
262
263class RubyGloballevel(RubyObject):
264    """
265    Description of an object on module level (functions, data).
266    """
267
268    def get_index_text(self, modname, name_cls):
269        if self.objtype == 'global':
270            return _('%s (global variable)') % name_cls[0]
271        else:
272            return ''
273
274
275class RubyEverywhere(RubyObject):
276    """
277    Description of a class member (methods, attributes).
278    """
279
280    def needs_arglist(self):
281        return self.objtype == 'method'
282
283    def get_index_text(self, modname, name_cls):
284        name, cls = name_cls
285        add_modules = self.env.config.add_module_names
286        if self.objtype == 'method':
287            try:
288                clsname, methname = ruby_rsplit(name)
289            except ValueError:
290                if modname:
291                    return _('%s() (in module %s)') % (name, modname)
292                else:
293                    return '%s()' % name
294            if modname and add_modules:
295                return _('%s() (%s::%s method)') % (methname, modname,
296                                                          clsname)
297            else:
298                return _('%s() (%s method)') % (methname, clsname)
299        else:
300            return ''
301
302
303class RubyClasslike(RubyObject):
304    """
305    Description of a class-like object (classes, exceptions).
306    """
307
308    def get_signature_prefix(self, sig):
309        return self.objtype + ' '
310
311    def get_index_text(self, modname, name_cls):
312        if self.objtype == 'class':
313            if not modname:
314                return _('%s (class)') % name_cls[0]
315            return _('%s (class in %s)') % (name_cls[0], modname)
316        elif self.objtype == 'exception':
317            return name_cls[0]
318        else:
319            return ''
320
321    def before_content(self):
322        RubyObject.before_content(self)
323        if self.names:
324            self.env.temp_data['rb:class'] = self.names[0][0]
325            self.clsname_set = True
326
327
328class RubyClassmember(RubyObject):
329    """
330    Description of a class member (methods, attributes).
331    """
332
333    def needs_arglist(self):
334        return self.objtype.endswith('method')
335
336    def get_signature_prefix(self, sig):
337        if self.objtype == 'classmethod':
338            return "classmethod %s." % self.class_name
339        elif self.objtype == 'attr_reader':
340            return "attribute [R] "
341        elif self.objtype == 'attr_writer':
342            return "attribute [W] "
343        elif self.objtype == 'attr_accessor':
344            return "attribute [R/W] "
345        return ''
346
347    def get_index_text(self, modname, name_cls):
348        name, cls = name_cls
349        add_modules = self.env.config.add_module_names
350        if self.objtype == 'classmethod':
351            try:
352                clsname, methname = ruby_rsplit(name)
353            except ValueError:
354                return '%s()' % name
355            if modname:
356                return _('%s() (%s.%s class method)') % (methname, modname,
357                                                         clsname)
358            else:
359                return _('%s() (%s class method)') % (methname, clsname)
360        elif self.objtype.startswith('attr'):
361            try:
362                clsname, attrname = ruby_rsplit(name)
363            except ValueError:
364                return name
365            if modname and add_modules:
366                return _('%s (%s.%s attribute)') % (attrname, modname, clsname)
367            else:
368                return _('%s (%s attribute)') % (attrname, clsname)
369        else:
370            return ''
371
372    def before_content(self):
373        RubyObject.before_content(self)
374        lastname = self.names and self.names[-1][1]
375        if lastname and not self.env.temp_data.get('rb:class'):
376            self.env.temp_data['rb:class'] = lastname.strip('.')
377            self.clsname_set = True
378
379
380class RubyModule(Directive):
381    """
382    Directive to mark description of a new module.
383    """
384
385    has_content = False
386    required_arguments = 1
387    optional_arguments = 0
388    final_argument_whitespace = False
389    option_spec = {
390        'platform': lambda x: x,
391        'synopsis': lambda x: x,
392        'noindex': directives.flag,
393        'deprecated': directives.flag,
394    }
395
396    def run(self):
397        env = self.state.document.settings.env
398        modname = self.arguments[0].strip()
399        noindex = 'noindex' in self.options
400        env.temp_data['rb:module'] = modname
401        env.domaindata['rb']['modules'][modname] = \
402            (env.docname, self.options.get('synopsis', ''),
403             self.options.get('platform', ''), 'deprecated' in self.options)
404        targetnode = nodes.target('', '', ids=['module-' + modname], ismod=True)
405        self.state.document.note_explicit_target(targetnode)
406        ret = [targetnode]
407        # XXX this behavior of the module directive is a mess...
408        if 'platform' in self.options:
409            platform = self.options['platform']
410            node = nodes.paragraph()
411            node += nodes.emphasis('', _('Platforms: '))
412            node += nodes.Text(platform, platform)
413            ret.append(node)
414        # the synopsis isn't printed; in fact, it is only used in the
415        # modindex currently
416        if not noindex:
417            indextext = _('%s (module)') % modname
418            inode = addnodes.index(entries=[_make_index(
419                'single', indextext, 'module-' + modname, modname)])
420            ret.append(inode)
421        return ret
422
423def _make_index(entrytype, entryname, target, ignored, key=None):
424    # Sphinx 1.4 introduced backward incompatible changes, it now
425    # requires 5 tuples.  Last one is categorization key.  See
426    # http://www.sphinx-doc.org/en/stable/extdev/nodes.html#sphinx.addnodes.index
427    if version_info >= (1, 4, 0, '', 0):
428        return (entrytype, entryname, target, ignored, key)
429    else:
430        return (entrytype, entryname, target, ignored)
431
432class RubyCurrentModule(Directive):
433    """
434    This directive is just to tell Sphinx that we're documenting
435    stuff in module foo, but links to module foo won't lead here.
436    """
437
438    has_content = False
439    required_arguments = 1
440    optional_arguments = 0
441    final_argument_whitespace = False
442    option_spec = {}
443
444    def run(self):
445        env = self.state.document.settings.env
446        modname = self.arguments[0].strip()
447        if modname == 'None':
448            env.temp_data['rb:module'] = None
449        else:
450            env.temp_data['rb:module'] = modname
451        return []
452
453
454class RubyXRefRole(XRefRole):
455    def process_link(self, env, refnode, has_explicit_title, title, target):
456        if not has_explicit_title:
457            title = title.lstrip('.')   # only has a meaning for the target
458            title = title.lstrip('#')
459            if title.startswith("::"):
460                title = title[2:]
461            target = target.lstrip('~') # only has a meaning for the title
462            # if the first character is a tilde, don't display the module/class
463            # parts of the contents
464            if title[0:1] == '~':
465                m = re.search(r"(?:\.)?(?:#)?(?:::)?(.*)\Z", title)
466                if m:
467                    title = m.group(1)
468        if not title.startswith("$"):
469            refnode['rb:module'] = env.temp_data.get('rb:module')
470            refnode['rb:class'] = env.temp_data.get('rb:class')
471        # if the first character is a dot, search more specific namespaces first
472        # else search builtins first
473        if target[0:1] == '.':
474            target = target[1:]
475            refnode['refspecific'] = True
476        return title, target
477
478
479class RubyModuleIndex(Index):
480    """
481    Index subclass to provide the Ruby module index.
482    """
483
484    name = 'modindex'
485    localname = _('Ruby Module Index')
486    shortname = _('modules')
487
488    def generate(self, docnames=None):
489        content = {}
490        # list of prefixes to ignore
491        ignores = self.domain.env.config['modindex_common_prefix']
492        ignores = sorted(ignores, key=len, reverse=True)
493        # list of all modules, sorted by module name
494        modules = sorted(_iteritems(self.domain.data['modules']),
495                         key=lambda x: x[0].lower())
496        # sort out collapsable modules
497        prev_modname = ''
498        num_toplevels = 0
499        for modname, (docname, synopsis, platforms, deprecated) in modules:
500            if docnames and docname not in docnames:
501                continue
502
503            for ignore in ignores:
504                if modname.startswith(ignore):
505                    modname = modname[len(ignore):]
506                    stripped = ignore
507                    break
508            else:
509                stripped = ''
510
511            # we stripped the whole module name?
512            if not modname:
513                modname, stripped = stripped, ''
514
515            entries = content.setdefault(modname[0].lower(), [])
516
517            package = modname.split('::')[0]
518            if package != modname:
519                # it's a submodule
520                if prev_modname == package:
521                    # first submodule - make parent a group head
522                    entries[-1][1] = 1
523                elif not prev_modname.startswith(package):
524                    # submodule without parent in list, add dummy entry
525                    entries.append([stripped + package, 1, '', '', '', '', ''])
526                subtype = 2
527            else:
528                num_toplevels += 1
529                subtype = 0
530
531            qualifier = deprecated and _('Deprecated') or ''
532            entries.append([stripped + modname, subtype, docname,
533                            'module-' + stripped + modname, platforms,
534                            qualifier, synopsis])
535            prev_modname = modname
536
537        # apply heuristics when to collapse modindex at page load:
538        # only collapse if number of toplevel modules is larger than
539        # number of submodules
540        collapse = len(modules) - num_toplevels < num_toplevels
541
542        # sort by first letter
543        content = sorted(_iteritems(content))
544
545        return content, collapse
546
547
548class RubyDomain(Domain):
549    """Ruby language domain."""
550    name = 'rb'
551    label = 'Ruby'
552    object_types = {
553        'function':        ObjType(_('function'),         'func', 'obj'),
554        'global':          ObjType(_('global variable'),  'global', 'obj'),
555        'method':          ObjType(_('method'),           'meth', 'obj'),
556        'class':           ObjType(_('class'),            'class', 'obj'),
557        'exception':       ObjType(_('exception'),        'exc', 'obj'),
558        'classmethod':     ObjType(_('class method'),     'meth', 'obj'),
559        'attr_reader':     ObjType(_('attribute'),        'attr', 'obj'),
560        'attr_writer':     ObjType(_('attribute'),        'attr', 'obj'),
561        'attr_accessor':   ObjType(_('attribute'),        'attr', 'obj'),
562        'const':           ObjType(_('const'),            'const', 'obj'),
563        'module':          ObjType(_('module'),           'mod', 'obj'),
564    }
565
566    directives = {
567        'function':        RubyModulelevel,
568        'global':          RubyGloballevel,
569        'method':          RubyEverywhere,
570        'const':           RubyEverywhere,
571        'class':           RubyClasslike,
572        'exception':       RubyClasslike,
573        'classmethod':     RubyClassmember,
574        'attr_reader':     RubyClassmember,
575        'attr_writer':     RubyClassmember,
576        'attr_accessor':   RubyClassmember,
577        'module':          RubyModule,
578        'currentmodule':   RubyCurrentModule,
579    }
580
581    roles = {
582        'func':  RubyXRefRole(fix_parens=False),
583        'global':RubyXRefRole(),
584        'class': RubyXRefRole(),
585        'exc':   RubyXRefRole(),
586        'meth':  RubyXRefRole(fix_parens=False),
587        'attr':  RubyXRefRole(),
588        'const': RubyXRefRole(),
589        'mod':   RubyXRefRole(),
590        'obj':   RubyXRefRole(),
591    }
592    initial_data = {
593        'objects': {},  # fullname -> docname, objtype
594        'modules': {},  # modname -> docname, synopsis, platform, deprecated
595    }
596    indices = [
597        RubyModuleIndex,
598    ]
599
600    def clear_doc(self, docname):
601        for fullname, (fn, _) in list(self.data['objects'].items()):
602            if fn == docname:
603                del self.data['objects'][fullname]
604        for modname, (fn, _, _, _) in list(self.data['modules'].items()):
605            if fn == docname:
606                del self.data['modules'][modname]
607
608    def find_obj(self, env, modname, classname, name, type, searchorder=0):
609        """
610        Find a Ruby object for "name", perhaps using the given module and/or
611        classname.
612        """
613        # skip parens
614        if name[-2:] == '()':
615            name = name[:-2]
616
617        if not name:
618            return None, None
619
620        objects = self.data['objects']
621
622        newname = None
623        if searchorder == 1:
624            if modname and classname and \
625                     modname + '::' + classname + '#' + name in objects:
626                newname = modname + '::' + classname + '#' + name
627            elif modname and classname and \
628                     modname + '::' + classname + '.' + name in objects:
629                newname = modname + '::' + classname + '.' + name
630            elif modname and modname + '::' + name in objects:
631                newname = modname + '::' + name
632            elif modname and modname + '#' + name in objects:
633                newname = modname + '#' + name
634            elif modname and modname + '.' + name in objects:
635                newname = modname + '.' + name
636            elif classname and classname + '.' + name in objects:
637                newname = classname + '.' + name
638            elif classname and classname + '#' + name in objects:
639                newname = classname + '#' + name
640            elif name in objects:
641                newname = name
642        else:
643            if name in objects:
644                newname = name
645            elif classname and classname + '.' + name in objects:
646                newname = classname + '.' + name
647            elif classname and classname + '#' + name in objects:
648                newname = classname + '#' + name
649            elif modname and modname + '::' + name in objects:
650                newname = modname + '::' + name
651            elif modname and modname + '#' + name in objects:
652                newname = modname + '#' + name
653            elif modname and modname + '.' + name in objects:
654                newname = modname + '.' + name
655            elif modname and classname and \
656                     modname + '::' + classname + '#' + name in objects:
657                newname = modname + '::' + classname + '#' + name
658            elif modname and classname and \
659                     modname + '::' + classname + '.' + name in objects:
660                newname = modname + '::' + classname + '.' + name
661            # special case: object methods
662            elif type in ('func', 'meth') and '.' not in name and \
663                 'object.' + name in objects:
664                newname = 'object.' + name
665        if newname is None:
666            return None, None
667        return newname, objects[newname]
668
669    def resolve_xref(self, env, fromdocname, builder,
670                     typ, target, node, contnode):
671        if (typ == 'mod' or
672            typ == 'obj' and target in self.data['modules']):
673            docname, synopsis, platform, deprecated = \
674                self.data['modules'].get(target, ('','','', ''))
675            if not docname:
676                return None
677            else:
678                title = '%s%s%s' % ((platform and '(%s) ' % platform),
679                                    synopsis,
680                                    (deprecated and ' (deprecated)' or ''))
681                return make_refnode(builder, fromdocname, docname,
682                                    'module-' + target, contnode, title)
683        else:
684            modname = node.get('rb:module')
685            clsname = node.get('rb:class')
686            searchorder = node.hasattr('refspecific') and 1 or 0
687            name, obj = self.find_obj(env, modname, clsname,
688                                      target, typ, searchorder)
689            if not obj:
690                return None
691            else:
692                return make_refnode(builder, fromdocname, obj[0], name,
693                                    contnode, name)
694
695    def get_objects(self):
696        for modname, info in _iteritems(self.data['modules']):
697            yield (modname, modname, 'module', info[0], 'module-' + modname, 0)
698        for refname, (docname, type) in _iteritems(self.data['objects']):
699            yield (refname, refname, type, docname, refname, 1)
700
701
702def setup(app):
703    app.add_domain(RubyDomain)
704