1 # -*- coding: utf-8 -*-
8 :copyright: Copyright 2010 by SHIBUKAWA Yoshiki
9 :license: BSD, see LICENSE for details.
14 from docutils import nodes
15 from docutils.parsers.rst import directives
16 from docutils.parsers.rst import Directive
18 from sphinx import addnodes
19 from sphinx import version_info
20 from sphinx.roles import XRefRole
21 from sphinx.locale import l_, _
22 from sphinx.domains import Domain, ObjType, Index
23 from sphinx.directives import ObjectDescription
24 from sphinx.util.nodes import make_refnode
25 from sphinx.util.docfields import Field, GroupedField, TypedField
27 # REs for Ruby signatures
28 rb_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
36 rb_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ','
39 'method':'#', 'attr_reader':'#', 'attr_writer':'#', 'attr_accessor':'#',
40 'function':'.', 'classmethod':'.', 'class':'::', 'module':'::',
41 'global':'', 'const':'::'}
43 rb_separator = re.compile(r"(?:\w+)?(?:::)?(?:\.)?(?:#)?")
52 def ruby_rsplit(fullname):
53 items = [item for item in rb_separator.findall(fullname)]
54 return ''.join(items[:-2]), items[-1]
57 class RubyObject(ObjectDescription):
59 Description of a general Ruby object.
62 'noindex': directives.flag,
63 'module': directives.unchanged,
67 TypedField('parameter', label=l_('Parameters'),
68 names=('param', 'parameter', 'arg', 'argument'),
69 typerolename='obj', typenames=('paramtype', 'type')),
70 TypedField('variable', label=l_('Variables'), rolename='obj',
71 names=('var', 'ivar', 'cvar'),
72 typerolename='obj', typenames=('vartype',)),
73 GroupedField('exceptions', label=l_('Raises'), rolename='exc',
74 names=('raises', 'raise', 'exception', 'except'),
76 Field('returnvalue', label=l_('Returns'), has_arg=False,
77 names=('returns', 'return')),
78 Field('returntype', label=l_('Return type'), has_arg=False,
82 def get_signature_prefix(self, sig):
84 May return a prefix to put before the object name in the signature.
88 def needs_arglist(self):
90 May return true if an empty argument list is to be generated even if
91 the document contains none.
95 def handle_signature(self, sig, signode):
97 Transform a Ruby signature into RST nodes.
98 Returns (fully qualified name of the thing, classname if any).
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
104 m = rb_sig_re.match(sig)
107 name_prefix, name, arglist, retann = m.groups()
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':
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('.')
126 separator = separators[self.objtype]
127 fullname = classname + separator + name_prefix + name
131 classname = name_prefix.rstrip('.')
132 fullname = name_prefix + name
137 signode['module'] = modname
138 signode['class'] = self.class_name = classname
139 signode['fullname'] = fullname
141 sig_prefix = self.get_signature_prefix(sig)
143 signode += addnodes.desc_annotation(sig_prefix, sig_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':
152 signode += addnodes.desc_addname(nodetext, nodetext)
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)
160 signode += addnodes.desc_name(name, name)
162 if self.needs_arglist():
163 # for callables, add an empty parameter list
164 signode += addnodes.desc_parameterlist()
166 signode += addnodes.desc_returns(retann, retann)
167 return fullname, name_prefix
168 signode += addnodes.desc_parameterlist()
170 stack = [signode[-1]]
171 for token in rb_paramlist_re.split(arglist):
173 opt = addnodes.desc_optional()
181 elif not token or token == ',' or token.isspace():
184 token = token.strip()
185 stack[-1] += addnodes.desc_parameter(token, token)
189 signode += addnodes.desc_returns(retann, retann)
190 return fullname, name_prefix
192 def get_index_text(self, modname, name):
194 Return the text for the index entry of the object.
196 raise NotImplementedError('must be implemented in subclasses')
198 def _is_class_member(self):
199 return self.objtype.endswith('method') or self.objtype.startswith('attr')
201 def add_target_and_index(self, name_cls, sig, signode):
202 if self.objtype == 'global':
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():
210 prefix = modname and modname + '::' or ''
212 prefix = modname and modname + separator or ''
214 prefix = modname and modname + separator or ''
215 fullname = prefix + name_cls[0]
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:
226 'duplicate object description of %s, ' % fullname +
227 'other instance in ' +
228 self.env.doc2path(objects[fullname][0]),
230 objects[fullname] = (self.env.docname, self.objtype)
232 indextext = self.get_index_text(modname, name_cls)
234 self.indexnode['entries'].append(
235 _make_index('single', indextext, fullname, fullname))
237 def before_content(self):
238 # needed for automatic qualification of members (reset in subclasses)
239 self.clsname_set = False
241 def after_content(self):
243 self.env.temp_data['rb:class'] = None
246 class RubyModulelevel(RubyObject):
248 Description of an object on module level (functions, data).
251 def needs_arglist(self):
252 return self.objtype == 'function'
254 def get_index_text(self, modname, name_cls):
255 if self.objtype == 'function':
257 return _('%s() (global function)') % name_cls[0]
258 return _('%s() (module function in %s)') % (name_cls[0], modname)
263 class RubyGloballevel(RubyObject):
265 Description of an object on module level (functions, data).
268 def get_index_text(self, modname, name_cls):
269 if self.objtype == 'global':
270 return _('%s (global variable)') % name_cls[0]
275 class RubyEverywhere(RubyObject):
277 Description of a class member (methods, attributes).
280 def needs_arglist(self):
281 return self.objtype == 'method'
283 def get_index_text(self, modname, name_cls):
285 add_modules = self.env.config.add_module_names
286 if self.objtype == 'method':
288 clsname, methname = ruby_rsplit(name)
291 return _('%s() (in module %s)') % (name, modname)
294 if modname and add_modules:
295 return _('%s() (%s::%s method)') % (methname, modname,
298 return _('%s() (%s method)') % (methname, clsname)
303 class RubyClasslike(RubyObject):
305 Description of a class-like object (classes, exceptions).
308 def get_signature_prefix(self, sig):
309 return self.objtype + ' '
311 def get_index_text(self, modname, name_cls):
312 if self.objtype == 'class':
314 return _('%s (class)') % name_cls[0]
315 return _('%s (class in %s)') % (name_cls[0], modname)
316 elif self.objtype == 'exception':
321 def before_content(self):
322 RubyObject.before_content(self)
324 self.env.temp_data['rb:class'] = self.names[0][0]
325 self.clsname_set = True
328 class RubyClassmember(RubyObject):
330 Description of a class member (methods, attributes).
333 def needs_arglist(self):
334 return self.objtype.endswith('method')
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] "
347 def get_index_text(self, modname, name_cls):
349 add_modules = self.env.config.add_module_names
350 if self.objtype == 'classmethod':
352 clsname, methname = ruby_rsplit(name)
356 return _('%s() (%s.%s class method)') % (methname, modname,
359 return _('%s() (%s class method)') % (methname, clsname)
360 elif self.objtype.startswith('attr'):
362 clsname, attrname = ruby_rsplit(name)
365 if modname and add_modules:
366 return _('%s (%s.%s attribute)') % (attrname, modname, clsname)
368 return _('%s (%s attribute)') % (attrname, clsname)
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
380 class RubyModule(Directive):
382 Directive to mark description of a new module.
386 required_arguments = 1
387 optional_arguments = 0
388 final_argument_whitespace = False
390 'platform': lambda x: x,
391 'synopsis': lambda x: x,
392 'noindex': directives.flag,
393 'deprecated': directives.flag,
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)
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)
414 # the synopsis isn't printed; in fact, it is only used in the
417 indextext = _('%s (module)') % modname
418 inode = addnodes.index(entries=[_make_index(
419 'single', indextext, 'module-' + modname, modname)])
423 def _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)
430 return (entrytype, entryname, target, ignored)
432 class RubyCurrentModule(Directive):
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.
439 required_arguments = 1
440 optional_arguments = 0
441 final_argument_whitespace = False
445 env = self.state.document.settings.env
446 modname = self.arguments[0].strip()
447 if modname == 'None':
448 env.temp_data['rb:module'] = None
450 env.temp_data['rb:module'] = modname
454 class 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("::"):
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)
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] == '.':
475 refnode['refspecific'] = True
479 class RubyModuleIndex(Index):
481 Index subclass to provide the Ruby module index.
485 localname = l_('Ruby Module Index')
486 shortname = l_('modules')
488 def generate(self, docnames=None):
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
499 for modname, (docname, synopsis, platforms, deprecated) in modules:
500 if docnames and docname not in docnames:
503 for ignore in ignores:
504 if modname.startswith(ignore):
505 modname = modname[len(ignore):]
511 # we stripped the whole module name?
513 modname, stripped = stripped, ''
515 entries = content.setdefault(modname[0].lower(), [])
517 package = modname.split('::')[0]
518 if package != modname:
520 if prev_modname == package:
521 # first submodule - make parent a group head
523 elif not prev_modname.startswith(package):
524 # submodule without parent in list, add dummy entry
525 entries.append([stripped + package, 1, '', '', '', '', ''])
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
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
542 # sort by first letter
543 content = sorted(_iteritems(content))
545 return content, collapse
548 class RubyDomain(Domain):
549 """Ruby language domain."""
553 'function': ObjType(l_('function'), 'func', 'obj'),
554 'global': ObjType(l_('global variable'), 'global', 'obj'),
555 'method': ObjType(l_('method'), 'meth', 'obj'),
556 'class': ObjType(l_('class'), 'class', 'obj'),
557 'exception': ObjType(l_('exception'), 'exc', 'obj'),
558 'classmethod': ObjType(l_('class method'), 'meth', 'obj'),
559 'attr_reader': ObjType(l_('attribute'), 'attr', 'obj'),
560 'attr_writer': ObjType(l_('attribute'), 'attr', 'obj'),
561 'attr_accessor': ObjType(l_('attribute'), 'attr', 'obj'),
562 'const': ObjType(l_('const'), 'const', 'obj'),
563 'module': ObjType(l_('module'), 'mod', 'obj'),
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,
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(),
593 'objects': {}, # fullname -> docname, objtype
594 'modules': {}, # modname -> docname, synopsis, platform, deprecated
600 def clear_doc(self, docname):
601 for fullname, (fn, _) in list(self.data['objects'].items()):
603 del self.data['objects'][fullname]
604 for modname, (fn, _, _, _) in list(self.data['modules'].items()):
606 del self.data['modules'][modname]
608 def find_obj(self, env, modname, classname, name, type, searchorder=0):
610 Find a Ruby object for "name", perhaps using the given module and/or
614 if name[-2:] == '()':
620 objects = self.data['objects']
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:
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
667 return newname, objects[newname]
669 def resolve_xref(self, env, fromdocname, builder,
670 typ, target, node, contnode):
672 typ == 'obj' and target in self.data['modules']):
673 docname, synopsis, platform, deprecated = \
674 self.data['modules'].get(target, ('','','', ''))
678 title = '%s%s%s' % ((platform and '(%s) ' % platform),
680 (deprecated and ' (deprecated)' or ''))
681 return make_refnode(builder, fromdocname, docname,
682 'module-' + target, contnode, title)
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)
692 return make_refnode(builder, fromdocname, obj[0], name,
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)
703 app.add_domain(RubyDomain)