Add nesting, module directive, and method directive to JavaScript domain

* Adds nesting to the JavaScript domain, to allow for nesting of elements
* Adds the ``js:module`` directive, which behaves similarly to the Python
  directive
* Adds the ``js:method`` directive, an alias to ``js:function``
* Adds roles for ``js:mod`` and ``js:meth``
* Updates tests to passing cases
* Adds docs for new features
This commit is contained in:
Anthony Johnson 2017-02-25 20:26:28 -08:00
parent b7cada236f
commit 3ba60ffd5d
5 changed files with 344 additions and 80 deletions

View File

@ -1103,6 +1103,22 @@ The JavaScript Domain
The JavaScript domain (name **js**) provides the following directives:
.. rst:directive:: .. js:module:: name
This directive sets the module name for object declarations that follow
after. The module name is used in the global module index and in cross
references. This directive does not create an object heading like
:rst:dir:`py:class` would, for example.
By default, this directive will create a linkable entity and will cause an
entry in the global module index, unless the ``noindex`` option is specified.
If this option is specified, the directive will only update the current
module name.
To clear the current module, set the module name to ``null`` or ``None``
.. versionadded:: 1.6
.. rst:directive:: .. js:function:: name(signature)
Describes a JavaScript function or method. If you want to describe
@ -1135,6 +1151,13 @@ The JavaScript domain (name **js**) provides the following directives:
:throws SomeError: For whatever reason in that case.
:returns: Something.
.. rst:directive:: .. js:method:: name(signature)
This directive is an alias for :rst:dir:`js:function`, however it describes a
function that is implemented as a method on a class object.
.. versionadded:: 1.6
.. rst:directive:: .. js:class:: name
Describes a constructor that creates an object. This is basically like
@ -1164,7 +1187,9 @@ The JavaScript domain (name **js**) provides the following directives:
These roles are provided to refer to the described objects:
.. rst:role:: js:func
.. rst:role:: js:mod
js:func
js:meth
js:class
js:data
js:attr

View File

@ -9,6 +9,9 @@
:license: BSD, see LICENSE for details.
"""
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from sphinx import addnodes
from sphinx.domains import Domain, ObjType
from sphinx.locale import l_, _
@ -38,57 +41,68 @@ class JSObject(ObjectDescription):
#: what is displayed right before the documentation entry
display_prefix = None # type: unicode
#: If ``allow_nesting`` is ``True``, the object prefixes will be accumulated
#: based on directive nesting
allow_nesting = False
def handle_signature(self, sig, signode):
# type: (unicode, addnodes.desc_signature) -> Tuple[unicode, unicode]
"""Breaks down construct signatures
Parses out prefix and argument list from construct definition. The
namespace and class will be determined by the nesting of domain
directives.
"""
sig = sig.strip()
if '(' in sig and sig[-1:] == ')':
prefix, arglist = sig.split('(', 1)
prefix = prefix.strip()
member, arglist = sig.split('(', 1)
member = member.strip()
arglist = arglist[:-1].strip()
else:
prefix = sig
member = sig
arglist = None
if '.' in prefix:
nameprefix, name = prefix.rsplit('.', 1)
else:
nameprefix = None
name = prefix
# If construct is nested, prefix the current prefix
prefix = self.env.ref_context.get('js:object', None)
mod_name = self.env.ref_context.get('js:module')
name = member
try:
member_prefix, member_name = member.rsplit('.', 1)
except ValueError:
member_name = name
member_prefix = ''
finally:
name = member_name
if prefix and member_prefix:
prefix = '.'.join([prefix, member_prefix])
elif prefix is None and member_prefix:
prefix = member_prefix
fullname = name
if prefix:
fullname = '.'.join([prefix, name])
objectname = self.env.ref_context.get('js:object')
if nameprefix:
if objectname:
# someone documenting the method of an attribute of the current
# object? shouldn't happen but who knows...
nameprefix = objectname + '.' + nameprefix
fullname = nameprefix + '.' + name
elif objectname:
fullname = objectname + '.' + name
else:
# just a function or constructor
objectname = ''
fullname = name
signode['object'] = objectname
signode['module'] = mod_name
signode['object'] = prefix
signode['fullname'] = fullname
if self.display_prefix:
signode += addnodes.desc_annotation(self.display_prefix,
self.display_prefix)
if nameprefix:
signode += addnodes.desc_addname(nameprefix + '.', nameprefix + '.')
if prefix:
signode += addnodes.desc_addname(prefix + '.', prefix + '.')
elif mod_name:
signode += addnodes.desc_addname(mod_name + '.', mod_name + '.')
signode += addnodes.desc_name(name, name)
if self.has_arguments:
if not arglist:
signode += addnodes.desc_parameterlist()
else:
_pseudo_parse_arglist(signode, arglist)
return fullname, nameprefix
return fullname, prefix
def add_target_and_index(self, name_obj, sig, signode):
# type: (Tuple[unicode, unicode], unicode, addnodes.desc_signature) -> None
objectname = self.options.get(
'object', self.env.ref_context.get('js:object'))
fullname = name_obj[0]
mod_name = self.env.ref_context.get('js:module')
fullname = (mod_name and mod_name + '.' or '') + name_obj[0]
if fullname not in self.state.document.ids:
signode['names'].append(fullname)
signode['ids'].append(fullname.replace('$', '_S_'))
@ -103,7 +117,7 @@ class JSObject(ObjectDescription):
line=self.lineno)
objects[fullname] = self.env.docname, self.objtype
indextext = self.get_index_text(objectname, name_obj)
indextext = self.get_index_text(mod_name, name_obj)
if indextext:
self.indexnode['entries'].append(('single', indextext,
fullname.replace('$', '_S_'),
@ -124,6 +138,68 @@ class JSObject(ObjectDescription):
return _('%s (%s attribute)') % (name, obj)
return ''
def before_content(self):
# type: () -> None
"""Handle object nesting before content
If this class is a nestable object, such as a class object, build up a
representation of the nesting heirarchy so that de-nesting multiple
levels works correctly.
If this class isn't a nestable object, just set the current object
prefix using the object name. This prefix will be removed with
:py:meth:`after_content`, and is not added to the list of nested
object prefixes.
The following keys are used in ``self.env.ref_context``:
js:objects
Stores the object prefix history. With each nested element, we
add the object prefix to this list. When we exit that object's
nesting level, :py:meth:`after_content` is triggered and the
prefix is removed from the end of the list.
js:object
Current object prefix. This should generally reflect the last
element in the prefix history
"""
prefix = None
if self.names:
(obj_name, obj_name_prefix) = self.names.pop()
prefix = obj_name_prefix.strip('.') if obj_name_prefix else None
if self.allow_nesting:
prefix = obj_name
if prefix:
self.env.ref_context['js:object'] = prefix
if self.allow_nesting:
try:
self.env.ref_context['js:objects'].append(prefix)
except (AttributeError, KeyError):
self.env.ref_context['js:objects'] = [prefix]
def after_content(self):
# type: () -> None
"""Handle object de-nesting after content
If this class is a nestable object, removing the last nested class prefix
ends further nesting in the object.
If this class is not a nestable object, the list of classes should not
be altered as we didn't affect the nesting levels in
:py:meth:`before_content`.
"""
if self.allow_nesting:
try:
self.env.ref_context['js:objects'].pop()
except (KeyError, IndexError):
self.env.ref_context['js:objects'] = []
try:
prefix = self.env.ref_context.get('js:objects', [])[-1]
except IndexError:
prefix = None
finally:
self.env.ref_context['js:object'] = prefix
class JSCallable(JSObject):
"""Description of a JavaScript function, method or constructor."""
@ -146,6 +222,61 @@ class JSCallable(JSObject):
class JSConstructor(JSCallable):
"""Like a callable but with a different prefix."""
display_prefix = 'class '
allow_nesting = True
class JSModule(Directive):
"""
Directive to mark description of a new JavaScript module.
This directive specifies the module name that will be used by objects that
follow this directive.
Options
-------
noindex
If the ``noindex`` option is specified, no linkable elements will be
created, and the module won't be added to the global module index. This
is useful for splitting up the module definition across multiple
sections or files.
:param mod_name: Module name. If the module name is ``nul``, or ``None``,
the module name will be cleared for objects that follow.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
'noindex': directives.flag
}
def run(self):
# type: () -> List[nodes.Node]
env = self.state.document.settings.env
mod_name = self.arguments[0].strip()
if mod_name in ['null', 'None']:
env.ref_context.pop('js:module', None)
return []
env.ref_context['js:module'] = mod_name
noindex = 'noindex' in self.options
ret = []
if not noindex:
env.domaindata['js']['modules'][mod_name] = env.docname
# Make a duplicate entry in 'objects' to facilitate searching for
# the module in JavaScriptDomain.find_obj()
env.domaindata['js']['objects'][mod_name] = (env.docname, 'module')
targetnode = nodes.target('', '', ids=['module-' + mod_name],
ismod=True)
self.state.document.note_explicit_target(targetnode)
ret.append(targetnode)
indextext = _('%s (module)') % mod_name
inode = addnodes.index(entries=[('single', indextext,
'module-' + mod_name, '', None)])
ret.append(inode)
return ret
class JSXRefRole(XRefRole):
@ -153,6 +284,7 @@ class JSXRefRole(XRefRole):
# type: (BuildEnvironment, nodes.Node, bool, unicode, unicode) -> Tuple[unicode, unicode] # NOQA
# basically what sphinx.domains.python.PyXRefRole does
refnode['js:object'] = env.ref_context.get('js:object')
refnode['js:module'] = env.ref_context.get('js:module')
if not has_explicit_title:
title = title.lstrip('.')
target = target.lstrip('~')
@ -174,31 +306,41 @@ class JavaScriptDomain(Domain):
# if you add a new object type make sure to edit JSObject.get_index_string
object_types = {
'function': ObjType(l_('function'), 'func'),
'method': ObjType(l_('method'), 'meth'),
'class': ObjType(l_('class'), 'class'),
'data': ObjType(l_('data'), 'data'),
'attribute': ObjType(l_('attribute'), 'attr'),
'module': ObjType(l_('module'), 'mod'),
}
directives = {
'function': JSCallable,
'method': JSCallable,
'class': JSConstructor,
'data': JSObject,
'attribute': JSObject,
'module': JSModule,
}
roles = {
'func': JSXRefRole(fix_parens=True),
'meth': JSXRefRole(fix_parens=True),
'class': JSXRefRole(fix_parens=True),
'data': JSXRefRole(),
'attr': JSXRefRole(),
'mod': JSXRefRole(),
}
initial_data = {
'objects': {}, # fullname -> docname, objtype
'modules': {}, # mod_name -> docname
} # type: Dict[unicode, Dict[unicode, Tuple[unicode, unicode]]]
def clear_doc(self, docname):
# type: (unicode) -> None
for fullname, (fn, _l) in list(self.data['objects'].items()):
if fn == docname:
for fullname, (pkg_docname, _l) in list(self.data['objects'].items()):
if pkg_docname == docname:
del self.data['objects'][fullname]
for mod_name, pkg_docname in list(self.data['modules'].items()):
if pkg_docname == docname:
del self.data['modules'][mod_name]
def merge_domaindata(self, docnames, otherdata):
# type: (List[unicode], Dict) -> None
@ -206,31 +348,42 @@ class JavaScriptDomain(Domain):
for fullname, (fn, objtype) in otherdata['objects'].items():
if fn in docnames:
self.data['objects'][fullname] = (fn, objtype)
for mod_name, pkg_docname in otherdata['modules'].items():
if pkg_docname in docnames:
self.data['modules'][mod_name] = pkg_docname
def find_obj(self, env, obj, name, typ, searchorder=0):
# type: (BuildEnvironment, unicode, unicode, unicode, int) -> Tuple[unicode, Tuple[unicode, unicode]] # NOQA
def find_obj(self, env, mod_name, prefix, name, typ, searchorder=0):
# type: (BuildEnvironment, unicode, unicode, unicode, unicode, int) -> Tuple[unicode, Tuple[unicode, unicode]] # NOQA
if name[-2:] == '()':
name = name[:-2]
objects = self.data['objects']
searches = []
if mod_name and prefix:
searches.append('.'.join([mod_name, prefix, name]))
if mod_name:
searches.append('.'.join([mod_name, name]))
if prefix:
searches.append('.'.join([prefix, name]))
searches.append(name)
if searchorder == 0:
searches.reverse()
newname = None
if searchorder == 1:
if obj and obj + '.' + name in objects:
newname = obj + '.' + name
else:
newname = name
else:
if name in objects:
newname = name
elif obj and obj + '.' + name in objects:
newname = obj + '.' + name
for search_name in searches:
if search_name in objects:
newname = search_name
return newname, objects.get(newname)
def resolve_xref(self, env, fromdocname, builder, typ, target, node,
contnode):
# type: (BuildEnvironment, unicode, Builder, unicode, unicode, nodes.Node, nodes.Node) -> nodes.Node # NOQA
objectname = node.get('js:object')
mod_name = node.get('js:module')
prefix = node.get('js:object')
searchorder = node.hasattr('refspecific') and 1 or 0
name, obj = self.find_obj(env, objectname, target, typ, searchorder)
name, obj = self.find_obj(env, mod_name, prefix, target, typ, searchorder)
if not obj:
return None
return make_refnode(builder, fromdocname, obj[0],
@ -239,8 +392,9 @@ class JavaScriptDomain(Domain):
def resolve_any_xref(self, env, fromdocname, builder, target, node,
contnode):
# type: (BuildEnvironment, unicode, Builder, unicode, nodes.Node, nodes.Node) -> List[Tuple[unicode, nodes.Node]] # NOQA
objectname = node.get('js:object')
name, obj = self.find_obj(env, objectname, target, None, 1)
mod_name = node.get('js:module')
prefix = node.get('js:object')
name, obj = self.find_obj(env, mod_name, prefix, target, None, 1)
if not obj:
return []
return [('js:' + self.role_for_objtype(obj[1]),

View File

@ -4,3 +4,4 @@ test-domain-js
.. toctree::
roles
package

View File

@ -0,0 +1,33 @@
module
=======
.. js:module:: module_a.submodule
* Link to :js:class:`ModTopLevel`
.. js:class:: ModTopLevel
* Link to :js:meth:`mod_child_1`
* Link to :js:meth:`ModTopLevel.mod_child_1`
.. js:method:: ModTopLevel.mod_child_1
* Link to :js:meth:`mod_child_2`
.. js:method:: ModTopLevel.mod_child_2
* Link to :js:meth:`module_a.submodule.ModTopLevel.mod_child_1`
.. js:module:: null
:noindex:
.. js:class:: ModNoModule
.. js:module:: module_b.submodule
* Link to :js:class:`ModTopLevel`
.. js:class:: ModTopLevel
* Link to :js:class:`ModNoModule`
* Link to :js:mod:`module_a.submodule`

View File

@ -20,7 +20,7 @@ def test_domain_js_xrefs(app, status, warning):
"""Domain objects have correct prefixes when looking up xrefs"""
app.builder.build_all()
def assert_refnode(node, class_name, target, reftype=None,
def assert_refnode(node, mod_name, prefix, target, reftype=None,
domain='js'):
attributes = {
'refdomain': domain,
@ -28,60 +28,111 @@ def test_domain_js_xrefs(app, status, warning):
}
if reftype is not None:
attributes['reftype'] = reftype
if class_name is not False:
attributes['js:class'] = class_name
if mod_name is not False:
attributes['js:module'] = mod_name
if prefix is not False:
attributes['js:object'] = prefix
assert_node(node, **attributes)
doctree = app.env.get_doctree('roles')
refnodes = list(doctree.traverse(addnodes.pending_xref))
assert_refnode(refnodes[0], False, u'TopLevel', u'class')
assert_refnode(refnodes[1], False, u'top_level', u'func')
assert_refnode(refnodes[2], False, u'child_1', u'func')
assert_refnode(refnodes[3], False, u'NestedChildA.subchild_2', u'func')
assert_refnode(refnodes[4], False, u'child_2', u'func')
assert_refnode(refnodes[5], False, u'any_child', domain='')
assert_refnode(refnodes[6], False, u'NestedChildA', u'class')
assert_refnode(refnodes[7], False, u'subchild_2', u'func')
assert_refnode(refnodes[8], False, u'NestedParentA.child_1', u'func')
assert_refnode(refnodes[9], False, u'NestedChildA.subchild_1', u'func')
assert_refnode(refnodes[10], False, u'child_1', u'func')
assert_refnode(refnodes[11], False, u'NestedParentB', u'class')
assert_refnode(refnodes[12], False, u'NestedParentA.NestedChildA', u'class')
assert_refnode(refnodes[0], None, None, u'TopLevel', u'class')
assert_refnode(refnodes[1], None, None, u'top_level', u'func')
assert_refnode(refnodes[2], None, u'NestedParentA', u'child_1', u'func')
assert_refnode(refnodes[3], None, u'NestedParentA',
u'NestedChildA.subchild_2', u'func')
assert_refnode(refnodes[4], None, u'NestedParentA', u'child_2', u'func')
assert_refnode(refnodes[5], False, u'NestedParentA', u'any_child', domain='')
assert_refnode(refnodes[6], None, u'NestedParentA', u'NestedChildA', u'class')
assert_refnode(refnodes[7], None, u'NestedParentA.NestedChildA',
u'subchild_2', u'func')
assert_refnode(refnodes[8], None, u'NestedParentA.NestedChildA',
u'NestedParentA.child_1', u'func')
assert_refnode(refnodes[9], None, u'NestedParentA',
u'NestedChildA.subchild_1', u'func')
assert_refnode(refnodes[10], None, u'NestedParentB', u'child_1', u'func')
assert_refnode(refnodes[11], None, u'NestedParentB', u'NestedParentB',
u'class')
assert_refnode(refnodes[12], None, None, u'NestedParentA.NestedChildA',
u'class')
assert len(refnodes) == 13
doctree = app.env.get_doctree('module')
refnodes = list(doctree.traverse(addnodes.pending_xref))
assert_refnode(refnodes[0], 'module_a.submodule', None, 'ModTopLevel',
'class')
assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel',
'mod_child_1', 'meth')
assert_refnode(refnodes[2], 'module_a.submodule', 'ModTopLevel',
'ModTopLevel.mod_child_1', 'meth')
assert_refnode(refnodes[3], 'module_a.submodule', 'ModTopLevel',
'mod_child_2', 'meth')
assert_refnode(refnodes[4], 'module_a.submodule', 'ModTopLevel',
'module_a.submodule.ModTopLevel.mod_child_1', 'meth')
assert_refnode(refnodes[5], 'module_b.submodule', None, 'ModTopLevel',
'class')
assert_refnode(refnodes[6], 'module_b.submodule', 'ModTopLevel',
'ModNoModule', 'class')
assert_refnode(refnodes[7], 'module_b.submodule', 'ModTopLevel',
'module_a.submodule', 'mod')
assert len(refnodes) == 8
@pytest.mark.sphinx('dummy', testroot='domain-js')
def test_domain_js_objects(app, status, warning):
app.builder.build_all()
modules = app.env.domains['js'].data['modules']
objects = app.env.domains['js'].data['objects']
assert 'module_a.submodule' in modules
assert 'module_a.submodule' in objects
assert 'module_b.submodule' in modules
assert 'module_b.submodule' in objects
assert objects['module_a.submodule.ModTopLevel'] == ('module', 'class')
assert objects['module_a.submodule.ModTopLevel.mod_child_1'] == ('module', 'method')
assert objects['module_a.submodule.ModTopLevel.mod_child_2'] == ('module', 'method')
assert objects['ModNoModule'] == ('module', 'class')
assert objects['module_b.submodule.ModTopLevel'] == ('module', 'class')
assert objects['TopLevel'] == ('roles', 'class')
assert objects['top_level'] == ('roles', 'function')
assert objects['NestedParentA'] == ('roles', 'class')
assert objects['child_1'] == ('roles', 'function')
assert objects['any_child'] == ('roles', 'function')
assert objects['NestedChildA'] == ('roles', 'class')
assert objects['subchild_1'] == ('roles', 'function')
assert objects['subchild_2'] == ('roles', 'function')
assert objects['child_2'] == ('roles', 'function')
assert objects['NestedParentA.child_1'] == ('roles', 'function')
assert objects['NestedParentA.any_child'] == ('roles', 'function')
assert objects['NestedParentA.NestedChildA'] == ('roles', 'class')
assert objects['NestedParentA.NestedChildA.subchild_1'] == ('roles', 'function')
assert objects['NestedParentA.NestedChildA.subchild_2'] == ('roles', 'function')
assert objects['NestedParentA.child_2'] == ('roles', 'function')
assert objects['NestedParentB'] == ('roles', 'class')
assert objects['NestedParentB.child_1'] == ('roles', 'function')
@pytest.mark.sphinx('dummy', testroot='domain-js')
def test_domain_js_find_obj(app, status, warning):
def find_obj(prefix, obj_name, obj_type, searchmode=0):
def find_obj(mod_name, prefix, obj_name, obj_type, searchmode=0):
return app.env.domains['js'].find_obj(
app.env, prefix, obj_name, obj_type, searchmode)
app.env, mod_name, prefix, obj_name, obj_type, searchmode)
app.builder.build_all()
assert (find_obj(None, u'NONEXISTANT', u'class') ==
assert (find_obj(None, None, u'NONEXISTANT', u'class') ==
(None, None))
assert (find_obj(None, u'TopLevel', u'class') ==
(u'TopLevel', (u'roles', u'class')))
assert (find_obj(None, u'NestedParentA.NestedChildA', u'class') ==
(None, None))
assert (find_obj(None, u'subchild_2', u'func') ==
(u'subchild_2', (u'roles', u'function')))
assert (find_obj(None, None, u'NestedParentA', u'class') ==
( u'NestedParentA', (u'roles', u'class')))
assert (find_obj(None, None, u'NestedParentA.NestedChildA', u'class') ==
( u'NestedParentA.NestedChildA', (u'roles', u'class')))
assert (find_obj(None, 'NestedParentA', u'NestedChildA', u'class') ==
( u'NestedParentA.NestedChildA', (u'roles', u'class')))
assert (find_obj(None, None, u'NestedParentA.NestedChildA.subchild_1', u'func') ==
( u'NestedParentA.NestedChildA.subchild_1', (u'roles', u'function')))
assert (find_obj(None, u'NestedParentA', u'NestedChildA.subchild_1', u'func') ==
( u'NestedParentA.NestedChildA.subchild_1', (u'roles', u'function')))
assert (find_obj(None, u'NestedParentA.NestedChildA', u'subchild_1', u'func') ==
( u'NestedParentA.NestedChildA.subchild_1', (u'roles', u'function')))
assert (find_obj(u'module_a.submodule', u'ModTopLevel', u'mod_child_2', u'meth') ==
( u'module_a.submodule.ModTopLevel.mod_child_2', (u'module', u'method')))
assert (find_obj(u'module_b.submodule', u'ModTopLevel', u'module_a.submodule', u'mod') ==
( u'module_a.submodule', (u'module', u'module')))