diff --git a/autoapi/domains/__init__.py b/autoapi/domains/__init__.py index db4b0ce..87db8a5 100644 --- a/autoapi/domains/__init__.py +++ b/autoapi/domains/__init__.py @@ -1,3 +1,4 @@ from .dotnet import DotNetDomain from .python import PythonDomain -from .go import GoDomain \ No newline at end of file +from .go import GoDomain +from .javascript import JavaScriptDomain \ No newline at end of file diff --git a/autoapi/domains/javascript.py b/autoapi/domains/javascript.py new file mode 100644 index 0000000..66ab91a --- /dev/null +++ b/autoapi/domains/javascript.py @@ -0,0 +1,201 @@ +import os +import json +import subprocess + +from sphinx.util.osutil import ensuredir + +from .base import AutoAPIBase, AutoAPIDomain +from ..settings import env + + +class JavaScriptDomain(AutoAPIDomain): + + '''Auto API domain handler for Javascript + + Parses directly from Javascript files. + + :param app: Sphinx application passed in as part of the extension + ''' + + def create_class(self, data): + '''Return instance of class based on Javascript data + + Data keys handled here: + + type + Set the object class + + consts, types, vars, funcs + Recurse into :py:meth:`create_class` to create child object + instances + + :param data: dictionary data from godocjson output + ''' + obj_map = dict( + (cls.type, cls) for cls + in ALL_CLASSES + ) + try: + cls = obj_map[data['kind']] + except KeyError: + self.app.warn('Unknown Type: %s' % data) + else: + # Recurse for children + obj = cls(data) + if 'children' in data: + for child_data in data['children']: + for child_obj in self.create_class(child_data): + obj.children.append(child_obj) + yield obj + + def read_file(self, path, **kwargs): + '''Read file input into memory, returning deserialized objects + + :param path: Path of file to read + ''' + # TODO support JSON here + # TODO sphinx way of reporting errors in logs? + + try: + parsed_data = json.loads(subprocess.check_output(['jsdoc', '-X', path])) + return parsed_data + except IOError: + print Warning('Error reading file: {0}'.format(path)) + except TypeError: + print Warning('Error reading file: {0}'.format(path)) + return None + + def get_objects(self, pattern, format='yaml'): + '''Trigger find of serialized sources and build objects''' + for path in self.find_files(pattern): + data = self.read_file(path, format=format) + if data: + # Returns a list of objects + for item in data: + for obj in self.create_class(item): + self.add_object(obj) + + def full(self): + self.get_objects(self.get_config('autoapi_file_pattern'), format='json') + self.generate_output() + self.write_indexes() + + def generate_output(self): + for obj in self.app.env.autoapi_data: + + if not obj: + continue + + rst = obj.render() + # Detail + try: + filename = obj.name.split('(')[0] + except IndexError: + filename = obj.name + detail_dir = os.path.join(self.get_config('autoapi_root'), + *filename.split('.')) + ensuredir(detail_dir) + # TODO: Better way to determine suffix? + path = os.path.join(detail_dir, '%s%s' % ('index', self.get_config('source_suffix')[0])) + if rst: + with open(path, 'w+') as detail_file: + detail_file.write(rst.encode('utf-8')) + + def write_indexes(self): + # Write Index + top_level_index = os.path.join(self.get_config('autoapi_root'), + 'index.rst') + with open(top_level_index, 'w+') as top_level_file: + content = env.get_template('index.rst') + top_level_file.write(content.render()) + + +class JavaScriptBase(AutoAPIBase): + + language = 'javascript' + + def __init__(self, obj): + super(JavaScriptBase, self).__init__(obj) + self.name = obj.get('name') + self.id = self.name + + # Second level + self.docstring = obj.get('description', '') + #self.docstring = obj.get('comment', '') + + self.imports = obj.get('imports', []) + self.children = [] + self.parameters = map( + lambda n: {'name': n['name'], + 'type': n['type'][0]}, + obj.get('param', []) + ) + + # Language Specific + pass + + def __str__(self): + return '<{cls} {id}>'.format(cls=self.__class__.__name__, + id=self.id) + + @property + def short_name(self): + '''Shorten name property''' + return self.name.split('.')[-1] + + @property + def namespace(self): + pieces = self.id.split('.')[:-1] + if pieces: + return '.'.join(pieces) + + @property + def ref_type(self): + return self.type + + @property + def ref_directive(self): + return self.type + + @property + def methods(self): + return self.obj.get('methods', []) + + def __lt__(self, other): + '''Sort object by name''' + if isinstance(other, JavaScriptBase): + return self.name.lower() < other.name.lower() + return self.name < other + + +class JavaScriptClass(JavaScriptBase): + type = 'class' + ref_directive = 'class' + + +class JavaScriptFunction(JavaScriptBase): + type = 'function' + ref_type = 'func' + + +class JavaScriptData(JavaScriptBase): + type = 'data' + ref_directive = 'data' + + +class JavaScriptMember(JavaScriptBase): + type = 'member' + ref_directive = 'member' + + +class JavaScriptAttribute(JavaScriptBase): + type = 'attribute' + ref_directive = 'attr' + +ALL_CLASSES = [ + JavaScriptFunction, + JavaScriptClass, + JavaScriptData, + JavaScriptAttribute, + JavaScriptMember, +] diff --git a/autoapi/extension.py b/autoapi/extension.py index f3613b3..3371717 100644 --- a/autoapi/extension.py +++ b/autoapi/extension.py @@ -6,7 +6,7 @@ Sphinx Auto-API import fnmatch import shutil -from .domains import DotNetDomain, PythonDomain, GoDomain +from .domains import DotNetDomain, PythonDomain, GoDomain, JavaScriptDomain def ignore_file(app, filename): @@ -32,6 +32,8 @@ def load_yaml(app): domain = PythonDomain(app) elif app.config.autoapi_type == 'go': domain = GoDomain(app) + elif app.config.autoapi_type == 'javascript': + domain = JavaScriptDomain(app) domain.full() diff --git a/autoapi/templates/javascript/class.rst b/autoapi/templates/javascript/class.rst new file mode 100644 index 0000000..7a44aab --- /dev/null +++ b/autoapi/templates/javascript/class.rst @@ -0,0 +1,20 @@ +.. js:class:: {{ obj.name }}{% if obj.args %}({{ obj.args|join(',') }}){% endif %} + + {% if obj.docstring %} + + .. rubric:: Summary + + {{ obj.docstring|indent(3) }} + + {% endif %} + + {% if obj.methods %} + + {% for method in obj.methods %} + + {% macro render() %}{{ method.render() }}{% endmacro %} + {{ render()|indent(3) }} + + {%- endfor %} + + {% endif %} diff --git a/autoapi/templates/javascript/function.rst b/autoapi/templates/javascript/function.rst new file mode 100644 index 0000000..88835ab --- /dev/null +++ b/autoapi/templates/javascript/function.rst @@ -0,0 +1,14 @@ +{# Identention in this file is important #} + +{% if is_method %} +{# Slice self off #} +.. method:: {{ obj.name.split('.')[-1] }}({{ args[1:]|join(',') }}) +{% else %} +.. function:: {{ obj.name.split('.')[-1] }}({{ args|join(',') }}) +{% endif %} + + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} + + diff --git a/autoapi/templates/javascript/member.rst b/autoapi/templates/javascript/member.rst new file mode 100644 index 0000000..8e2d86a --- /dev/null +++ b/autoapi/templates/javascript/member.rst @@ -0,0 +1,7 @@ +{# Identention in this file is important #} + +.. {{ obj.type }}:: {{ obj.name }} + + {{ obj.docstring|indent(3) }} + + diff --git a/autoapi/templates/javascript/module.rst b/autoapi/templates/javascript/module.rst new file mode 100644 index 0000000..398a069 --- /dev/null +++ b/autoapi/templates/javascript/module.rst @@ -0,0 +1,52 @@ +{{ obj.name }} +{{ "-" * obj.name|length }} + +{% block toc %} + +{% if obj.children %} + +.. toctree:: + :maxdepth: 4 + + {% for item in obj.children|sort %} + /autoapi/{{ item.id.split('.')|join('/') }}/index + {%- endfor %} + +{% endif %} + +{% endblock %} + +{% if obj.docstring %} + +.. rubric:: Summary + +{{ obj.docstring }} + +{% endif %} + +.. js:module:: {{ obj.name }} + + + +{% block content %} + +{%- macro display_type(item_type) %} + +{{ item_type.title() }} +{{ "*" * item_type|length }} + +{%- for obj_item in obj.item_map.get(item_type, []) %} +{% macro render() %}{{ obj_item.render() }}{% endmacro %} + + {{ render()|indent(4) }} + +{%- endfor %} +{%- endmacro %} + +{%- for item_type in obj.item_map.keys() %} +{% if item_type.lower() != 'module' %} +{{ display_type(item_type) }} +{% endif %} +{%- endfor %} + +{% endblock %} diff --git a/tests/jsexample/conf.py b/tests/jsexample/conf.py new file mode 100644 index 0000000..1875806 --- /dev/null +++ b/tests/jsexample/conf.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +templates_path = ['_templates'] +source_suffix = '.rst' +master_doc = 'index' +project = u'jsexample' +copyright = u'2015, rtfd' +author = u'rtfd' +version = '0.1' +release = '0.1' +language = None +exclude_patterns = ['_build'] +pygments_style = 'sphinx' +todo_include_todos = False +html_theme = 'alabaster' +html_static_path = ['_static'] +htmlhelp_basename = 'jsexampledoc' +extensions = ['autoapi.extension'] +autoapi_type = 'javascript' +autoapi_dir = 'example' +autoapi_file_pattern = '*.js' \ No newline at end of file diff --git a/tests/jsexample/example/jsdoc-example.js b/tests/jsexample/example/jsdoc-example.js new file mode 100644 index 0000000..e633024 --- /dev/null +++ b/tests/jsexample/example/jsdoc-example.js @@ -0,0 +1,53 @@ +/** + * Creates an instance of Circle. + * + * @constructor + * @this {Circle} + * @param {number} r The desired radius of the circle. + */ +function Circle(r) { + /** @private */ this.radius = r; + /** @private */ this.circumference = 2 * Math.PI * r; +} + +/** + * Creates a new Circle from a diameter. + * + * @param {number} d The desired diameter of the circle. + * @return {Circle} The new Circle object. + */ +Circle.fromDiameter = function (d) { + return new Circle(d / 2); +}; + +/** + * Calculates the circumference of the Circle. + * + * @deprecated + * @this {Circle} + * @return {number} The circumference of the circle. + */ +Circle.prototype.calculateCircumference = function () { + return 2 * Math.PI * this.radius; +}; + +/** + * Returns the pre-computed circumference of the Circle. + * + * @this {Circle} + * @return {number} The circumference of the circle. + */ +Circle.prototype.getCircumference = function () { + return this.circumference; +}; + +/** + * Find a String representation of the Circle. + * + * @override + * @this {Circle} + * @return {string} Human-readable representation of this Circle. + */ +Circle.prototype.toString = function () { + return "A Circle object with radius of " + this.radius + "."; +}; diff --git a/tests/jsexample/index.rst b/tests/jsexample/index.rst new file mode 100644 index 0000000..9f26e84 --- /dev/null +++ b/tests/jsexample/index.rst @@ -0,0 +1,21 @@ +Welcome to jsexample's documentation! +===================================== + +.. toctree:: + + autoapi/index + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/tests/test_python_full.py b/tests/test_python_full.py index 0739c4d..2054053 100644 --- a/tests/test_python_full.py +++ b/tests/test_python_full.py @@ -31,3 +31,20 @@ Function finally: os.chdir('../..') + + +class FullJavaScriptTests(unittest.TestCase): + + def test_full_run(self): + os.chdir('tests/jsexample') + try: + if os.path.exists('_build'): + shutil.rmtree('_build') + os.mkdir('_build') + sp.check_call('sphinx-build -b text -d ./doctrees . _build/text', shell=True) + + with open('_build/text/autoapi/Circle/index.txt') as fin: + text = fin.read().strip() + self.assertIn('Creates an instance of Circle', text) + finally: + os.chdir('../..')