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