summaryrefslogtreecommitdiffstats
path: root/sphinx/domains/javascript.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/domains/javascript.py')
-rw-r--r--sphinx/domains/javascript.py508
1 files changed, 508 insertions, 0 deletions
diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py
new file mode 100644
index 0000000..75149c3
--- /dev/null
+++ b/sphinx/domains/javascript.py
@@ -0,0 +1,508 @@
+"""The JavaScript domain."""
+
+from __future__ import annotations
+
+import contextlib
+from typing import TYPE_CHECKING, Any, cast
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+
+from sphinx import addnodes
+from sphinx.directives import ObjectDescription
+from sphinx.domains import Domain, ObjType
+from sphinx.domains.python import _pseudo_parse_arglist
+from sphinx.locale import _, __
+from sphinx.roles import XRefRole
+from sphinx.util import logging
+from sphinx.util.docfields import Field, GroupedField, TypedField
+from sphinx.util.docutils import SphinxDirective
+from sphinx.util.nodes import make_id, make_refnode, nested_parse_with_titles
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+
+ from docutils.nodes import Element, Node
+
+ from sphinx.addnodes import desc_signature, pending_xref
+ from sphinx.application import Sphinx
+ from sphinx.builders import Builder
+ from sphinx.environment import BuildEnvironment
+ from sphinx.util.typing import OptionSpec
+
+logger = logging.getLogger(__name__)
+
+
+class JSObject(ObjectDescription[tuple[str, str]]):
+ """
+ Description of a JavaScript object.
+ """
+ #: If set to ``True`` this object is callable and a `desc_parameterlist` is
+ #: added
+ has_arguments = False
+
+ #: If ``allow_nesting`` is ``True``, the object prefixes will be accumulated
+ #: based on directive nesting
+ allow_nesting = False
+
+ option_spec: OptionSpec = {
+ 'no-index': directives.flag,
+ 'no-index-entry': directives.flag,
+ 'no-contents-entry': directives.flag,
+ 'no-typesetting': directives.flag,
+ 'noindex': directives.flag,
+ 'noindexentry': directives.flag,
+ 'nocontentsentry': directives.flag,
+ 'single-line-parameter-list': directives.flag,
+ }
+
+ def get_display_prefix(self) -> list[Node]:
+ #: what is displayed right before the documentation entry
+ return []
+
+ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
+ """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:] == ')':
+ member, arglist = sig.split('(', 1)
+ member = member.strip()
+ arglist = arglist[:-1].strip()
+ else:
+ member = sig
+ arglist = None
+ # 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])
+
+ signode['module'] = mod_name
+ signode['object'] = prefix
+ signode['fullname'] = fullname
+
+ max_len = (self.env.config.javascript_maximum_signature_line_length
+ or self.env.config.maximum_signature_line_length
+ or 0)
+ multi_line_parameter_list = (
+ 'single-line-parameter-list' not in self.options
+ and (len(sig) > max_len > 0)
+ )
+
+ display_prefix = self.get_display_prefix()
+ if display_prefix:
+ signode += addnodes.desc_annotation('', '', *display_prefix)
+
+ actual_prefix = None
+ if prefix:
+ actual_prefix = prefix
+ elif mod_name:
+ actual_prefix = mod_name
+ if actual_prefix:
+ addName = addnodes.desc_addname('', '')
+ for p in actual_prefix.split('.'):
+ addName += addnodes.desc_sig_name(p, p)
+ addName += addnodes.desc_sig_punctuation('.', '.')
+ signode += addName
+ signode += addnodes.desc_name('', '', addnodes.desc_sig_name(name, name))
+ if self.has_arguments:
+ if not arglist:
+ signode += addnodes.desc_parameterlist()
+ else:
+ _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
+ return fullname, prefix
+
+ def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]:
+ if 'fullname' not in sig_node:
+ return ()
+ modname = sig_node.get('module')
+ fullname = sig_node['fullname']
+
+ if modname:
+ return (modname, *fullname.split('.'))
+ else:
+ return tuple(fullname.split('.'))
+
+ def add_target_and_index(self, name_obj: tuple[str, str], sig: str,
+ signode: desc_signature) -> None:
+ mod_name = self.env.ref_context.get('js:module')
+ fullname = (mod_name + '.' if mod_name else '') + name_obj[0]
+ node_id = make_id(self.env, self.state.document, '', fullname)
+ signode['ids'].append(node_id)
+ self.state.document.note_explicit_target(signode)
+
+ domain = cast(JavaScriptDomain, self.env.get_domain('js'))
+ domain.note_object(fullname, self.objtype, node_id, location=signode)
+
+ if 'no-index-entry' not in self.options:
+ indextext = self.get_index_text(mod_name, name_obj) # type: ignore[arg-type]
+ if indextext:
+ self.indexnode['entries'].append(('single', indextext, node_id, '', None))
+
+ def get_index_text(self, objectname: str, name_obj: tuple[str, str]) -> str:
+ name, obj = name_obj
+ if self.objtype == 'function':
+ if not obj:
+ return _('%s() (built-in function)') % name
+ return _('%s() (%s method)') % (name, obj)
+ elif self.objtype == 'class':
+ return _('%s() (class)') % name
+ elif self.objtype == 'data':
+ return _('%s (global variable or constant)') % name
+ elif self.objtype == 'attribute':
+ return _('%s (%s attribute)') % (name, obj)
+ return ''
+
+ def before_content(self) -> None:
+ """Handle object nesting before content
+
+ :py:class:`JSObject` represents JavaScript language constructs. For
+ constructs that are nestable, this method will build up a stack of the
+ nesting hierarchy so that it can be later de-nested correctly, in
+ :py:meth:`after_content`.
+
+ For constructs that aren't nestable, the stack is bypassed, and instead
+ only the most recent object is tracked. This object prefix name will be
+ removed with :py:meth:`after_content`.
+
+ 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:
+ objects = self.env.ref_context.setdefault('js:objects', [])
+ objects.append(prefix)
+
+ def after_content(self) -> 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`.
+ """
+ objects = self.env.ref_context.setdefault('js:objects', [])
+ if self.allow_nesting:
+ with contextlib.suppress(IndexError):
+ objects.pop()
+
+ self.env.ref_context['js:object'] = (objects[-1] if len(objects) > 0
+ else None)
+
+ def _toc_entry_name(self, sig_node: desc_signature) -> str:
+ if not sig_node.get('_toc_parts'):
+ return ''
+
+ config = self.env.app.config
+ objtype = sig_node.parent.get('objtype')
+ if config.add_function_parentheses and objtype in {'function', 'method'}:
+ parens = '()'
+ else:
+ parens = ''
+ *parents, name = sig_node['_toc_parts']
+ if config.toc_object_entries_show_parents == 'domain':
+ return sig_node.get('fullname', name) + parens
+ if config.toc_object_entries_show_parents == 'hide':
+ return name + parens
+ if config.toc_object_entries_show_parents == 'all':
+ return '.'.join(parents + [name + parens])
+ return ''
+
+
+class JSCallable(JSObject):
+ """Description of a JavaScript function, method or constructor."""
+ has_arguments = True
+
+ doc_field_types = [
+ TypedField('arguments', label=_('Arguments'),
+ names=('argument', 'arg', 'parameter', 'param'),
+ typerolename='func', typenames=('paramtype', 'type')),
+ GroupedField('errors', label=_('Throws'), rolename='func',
+ names=('throws', ),
+ can_collapse=True),
+ Field('returnvalue', label=_('Returns'), has_arg=False,
+ names=('returns', 'return')),
+ Field('returntype', label=_('Return type'), has_arg=False,
+ names=('rtype',)),
+ ]
+
+
+class JSConstructor(JSCallable):
+ """Like a callable but with a different prefix."""
+
+ allow_nesting = True
+
+ def get_display_prefix(self) -> list[Node]:
+ return [addnodes.desc_sig_keyword('class', 'class'),
+ addnodes.desc_sig_space()]
+
+
+class JSModule(SphinxDirective):
+ """
+ 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
+ -------
+
+ no-index
+ If the ``:no-index:`` 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
+ """
+
+ has_content = True
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec: OptionSpec = {
+ 'no-index': directives.flag,
+ 'no-contents-entry': directives.flag,
+ 'no-typesetting': directives.flag,
+ 'noindex': directives.flag,
+ 'nocontentsentry': directives.flag,
+ }
+
+ def run(self) -> list[Node]:
+ mod_name = self.arguments[0].strip()
+ self.env.ref_context['js:module'] = mod_name
+ no_index = 'no-index' in self.options or 'noindex' in self.options
+
+ content_node: Element = nodes.section()
+ # necessary so that the child nodes get the right source/line set
+ content_node.document = self.state.document
+ nested_parse_with_titles(self.state, self.content, content_node, self.content_offset)
+
+ ret: list[Node] = []
+ if not no_index:
+ domain = cast(JavaScriptDomain, self.env.get_domain('js'))
+
+ node_id = make_id(self.env, self.state.document, 'module', mod_name)
+ domain.note_module(mod_name, node_id)
+ # Make a duplicate entry in 'objects' to facilitate searching for
+ # the module in JavaScriptDomain.find_obj()
+ domain.note_object(mod_name, 'module', node_id,
+ location=(self.env.docname, self.lineno))
+
+ # The node order is: index node first, then target node
+ indextext = _('%s (module)') % mod_name
+ inode = addnodes.index(entries=[('single', indextext, node_id, '', None)])
+ ret.append(inode)
+ target = nodes.target('', '', ids=[node_id], ismod=True)
+ self.state.document.note_explicit_target(target)
+ ret.append(target)
+ ret.extend(content_node.children)
+ return ret
+
+
+class JSXRefRole(XRefRole):
+ def process_link(self, env: BuildEnvironment, refnode: Element,
+ has_explicit_title: bool, title: str, target: str) -> tuple[str, str]:
+ # 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('~')
+ if title[0:1] == '~':
+ title = title[1:]
+ dot = title.rfind('.')
+ if dot != -1:
+ title = title[dot + 1:]
+ if target[0:1] == '.':
+ target = target[1:]
+ refnode['refspecific'] = True
+ return title, target
+
+
+class JavaScriptDomain(Domain):
+ """JavaScript language domain."""
+ name = 'js'
+ label = 'JavaScript'
+ # if you add a new object type make sure to edit JSObject.get_index_string
+ object_types = {
+ 'function': ObjType(_('function'), 'func'),
+ 'method': ObjType(_('method'), 'meth'),
+ 'class': ObjType(_('class'), 'class'),
+ 'data': ObjType(_('data'), 'data'),
+ 'attribute': ObjType(_('attribute'), 'attr'),
+ 'module': ObjType(_('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: dict[str, dict[str, tuple[str, str]]] = {
+ 'objects': {}, # fullname -> docname, node_id, objtype
+ 'modules': {}, # modname -> docname, node_id
+ }
+
+ @property
+ def objects(self) -> dict[str, tuple[str, str, str]]:
+ return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype
+
+ def note_object(self, fullname: str, objtype: str, node_id: str,
+ location: Any = None) -> None:
+ if fullname in self.objects:
+ docname = self.objects[fullname][0]
+ logger.warning(__('duplicate %s description of %s, other %s in %s'),
+ objtype, fullname, objtype, docname, location=location)
+ self.objects[fullname] = (self.env.docname, node_id, objtype)
+
+ @property
+ def modules(self) -> dict[str, tuple[str, str]]:
+ return self.data.setdefault('modules', {}) # modname -> docname, node_id
+
+ def note_module(self, modname: str, node_id: str) -> None:
+ self.modules[modname] = (self.env.docname, node_id)
+
+ def clear_doc(self, docname: str) -> None:
+ for fullname, (pkg_docname, _node_id, _l) in list(self.objects.items()):
+ if pkg_docname == docname:
+ del self.objects[fullname]
+ for modname, (pkg_docname, _node_id) in list(self.modules.items()):
+ if pkg_docname == docname:
+ del self.modules[modname]
+
+ def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> None:
+ # XXX check duplicates
+ for fullname, (fn, node_id, objtype) in otherdata['objects'].items():
+ if fn in docnames:
+ self.objects[fullname] = (fn, node_id, objtype)
+ for mod_name, (pkg_docname, node_id) in otherdata['modules'].items():
+ if pkg_docname in docnames:
+ self.modules[mod_name] = (pkg_docname, node_id)
+
+ def find_obj(
+ self,
+ env: BuildEnvironment,
+ mod_name: str,
+ prefix: str,
+ name: str,
+ typ: str | None,
+ searchorder: int = 0,
+ ) -> tuple[str | None, tuple[str, str, str] | None]:
+ if name[-2:] == '()':
+ name = name[:-2]
+
+ 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
+ object_ = None
+ for search_name in searches:
+ if search_name in self.objects:
+ newname = search_name
+ object_ = self.objects[search_name]
+
+ return newname, object_
+
+ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
+ typ: str, target: str, node: pending_xref, contnode: Element,
+ ) -> Element | None:
+ mod_name = node.get('js:module')
+ prefix = node.get('js:object')
+ searchorder = 1 if node.hasattr('refspecific') else 0
+ name, obj = self.find_obj(env, mod_name, prefix, target, typ, searchorder)
+ if not obj:
+ return None
+ return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name)
+
+ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
+ target: str, node: pending_xref, contnode: Element,
+ ) -> list[tuple[str, Element]]:
+ 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[2]), # type: ignore[operator]
+ make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))]
+
+ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
+ for refname, (docname, node_id, typ) in list(self.objects.items()):
+ yield refname, refname, typ, docname, node_id, 1
+
+ def get_full_qualified_name(self, node: Element) -> str | None:
+ modname = node.get('js:module')
+ prefix = node.get('js:object')
+ target = node.get('reftarget')
+ if target is None:
+ return None
+ else:
+ return '.'.join(filter(None, [modname, prefix, target]))
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ app.add_domain(JavaScriptDomain)
+ app.add_config_value(
+ 'javascript_maximum_signature_line_length', None, 'env', types={int, None},
+ )
+ return {
+ 'version': 'builtin',
+ 'env_version': 3,
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }