From 3ca87334942dccd7b22aeea3ae278728bfb4d323 Mon Sep 17 00:00:00 2001
From: ch3 <ch3@mailbox.org>
Date: Tue, 9 Jul 2019 20:32:25 +0200
Subject: [PATCH] doc: Vendor hawkmoth in a subdirectory

Co-authored-by:  Rahix <rahix@rahix.de>
---
 Documentation/conf.py                    |   8 +-
 Documentation/hawkmoth/LICENSE           |  25 ++
 Documentation/hawkmoth/Makefile          |   5 +
 Documentation/hawkmoth/Makefile.local    |   5 +
 Documentation/hawkmoth/README.txt        | 121 +++++++++
 Documentation/hawkmoth/VERSION           |   1 +
 Documentation/hawkmoth/__init__.py       | 119 +++++++++
 Documentation/hawkmoth/__main__.py       |  45 ++++
 Documentation/hawkmoth/parser.py         | 301 +++++++++++++++++++++++
 Documentation/hawkmoth/util/__init__.py  |   0
 Documentation/hawkmoth/util/doccompat.py |  60 +++++
 Documentation/hawkmoth/util/docstr.py    | 101 ++++++++
 12 files changed, 788 insertions(+), 3 deletions(-)
 create mode 100644 Documentation/hawkmoth/LICENSE
 create mode 100644 Documentation/hawkmoth/Makefile
 create mode 100644 Documentation/hawkmoth/Makefile.local
 create mode 100644 Documentation/hawkmoth/README.txt
 create mode 100644 Documentation/hawkmoth/VERSION
 create mode 100644 Documentation/hawkmoth/__init__.py
 create mode 100644 Documentation/hawkmoth/__main__.py
 create mode 100644 Documentation/hawkmoth/parser.py
 create mode 100644 Documentation/hawkmoth/util/__init__.py
 create mode 100644 Documentation/hawkmoth/util/doccompat.py
 create mode 100644 Documentation/hawkmoth/util/docstr.py

diff --git a/Documentation/conf.py b/Documentation/conf.py
index 3c12ec794..8bdf2933d 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -9,6 +9,7 @@ import sphinx.util.logging
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
 sys.path.insert(0, os.path.abspath("../pycardium/modules/py"))
+sys.path.insert(0, os.path.abspath("./"))
 
 logger = sphinx.util.logging.getLogger("card10/conf.py")
 
@@ -41,7 +42,7 @@ extensions = [
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ["output", "Thumbs.db", ".DS_Store"]
+exclude_patterns = ["output", "Thumbs.db", ".DS_Store", "hawkmoth"]
 
 # -- Options for HTML output ------------------------------------------------- {{{
 
@@ -83,8 +84,9 @@ try:
 
     cautodoc_root = os.path.abspath("..")
     has_hawkmoth = True
-except ImportError:
-    logger.warning("Hawkmoth was not found.  Documentation for Epicardium API will not be generated.")
+except ImportError as e:
+    if e.name == "clang":
+        logger.warning("hawkmoth requires the clang python module.  Documentation for Epicardium API will not be generated.")
 # }}}
 
 
diff --git a/Documentation/hawkmoth/LICENSE b/Documentation/hawkmoth/LICENSE
new file mode 100644
index 000000000..6b0f4aa2d
--- /dev/null
+++ b/Documentation/hawkmoth/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2016-2017, Jani Nikula <jani@nikula.org>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Documentation/hawkmoth/Makefile b/Documentation/hawkmoth/Makefile
new file mode 100644
index 000000000..fa25832e0
--- /dev/null
+++ b/Documentation/hawkmoth/Makefile
@@ -0,0 +1,5 @@
+all:
+	$(MAKE) -C .. all
+
+.DEFAULT:
+	$(MAKE) -C .. $@
diff --git a/Documentation/hawkmoth/Makefile.local b/Documentation/hawkmoth/Makefile.local
new file mode 100644
index 000000000..c6fdecb03
--- /dev/null
+++ b/Documentation/hawkmoth/Makefile.local
@@ -0,0 +1,5 @@
+# -*- makefile -*-
+
+dir := hawkmoth
+
+CLEAN := $(CLEAN) $(dir)/hawkmoth.pyc $(dir)/cautodoc.pyc $(dir)/__init__.pyc
diff --git a/Documentation/hawkmoth/README.txt b/Documentation/hawkmoth/README.txt
new file mode 100644
index 000000000..118ce99e2
--- /dev/null
+++ b/Documentation/hawkmoth/README.txt
@@ -0,0 +1,121 @@
+Hawkmoth - Sphinx Autodoc for C
+===============================
+
+Hawkmoth is a minimalistic Sphinx_ `C Domain`_ autodoc directive extension to
+incorporate formatted C source code comments written in reStructuredText_ into
+Sphinx based documentation. It uses Clang Python Bindings for parsing, and
+generates C Domain directives for C API documentation, and more. In short,
+Hawkmoth is Sphinx Autodoc for C.
+
+Hawkmoth aims to be a compelling alternative for documenting C projects using
+Sphinx, mainly through its simplicity of design, implementation and use.
+
+.. _Sphinx: http://www.sphinx-doc.org
+
+.. _C Domain: http://www.sphinx-doc.org/en/stable/domains.html
+
+.. _reStructuredText: http://docutils.sourceforge.net/rst.html
+
+Example
+-------
+
+Given C source code with rather familiar looking documentation comments::
+
+  /**
+   * Get foo out of bar.
+   */
+  void foobar();
+
+and a directive in the Sphinx project::
+
+  .. c:autodoc:: filename.c
+
+you can incorporate code documentation into Sphinx. It's as simple as that.
+
+You can document functions, their parameters and return values, structs, unions,
+their members, macros, function-like macros, enums, enumeration constants,
+typedefs, variables, as well as have generic documentation comments not attached
+to any symbols.
+
+Documentation
+-------------
+
+Documentation on how to configure Hawkmoth and write documentation comments,
+with examples, is available in the ``doc`` directory in the source tree,
+obviously in Sphinx format and using the directive extension. Pre-built
+documentation `showcasing what Hawkmoth can do`_ is available at `Read the
+Docs`_.
+
+.. _showcasing what Hawkmoth can do: https://hawkmoth.readthedocs.io/en/latest/examples.html
+
+.. _Read the Docs: https://hawkmoth.readthedocs.io/
+
+Installation
+------------
+
+You can install Hawkmoth from PyPI_ with::
+
+  pip install hawkmoth
+
+You'll additionally need to install Clang and Python 3 bindings for it through
+your distro's package manager; they are not available via PyPI. You may also
+need to set ``LD_LIBRARY_PATH`` so that the Clang library can be found. For
+example::
+
+  export LD_LIBRARY_PATH=$(llvm-config --libdir)
+
+Alternatively, installation packages are available for:
+
+* `Arch Linux`_
+
+In Sphinx ``conf.py``, add ``hawkmoth`` to ``extensions``, and point
+``cautodoc_root`` at the source tree. See the extension documentation for
+details.
+
+.. _PyPI: https://pypi.org/project/hawkmoth/
+
+.. _Arch Linux: https://aur.archlinux.org/packages/?K=hawkmoth
+
+Development and Contributing
+----------------------------
+
+Hawkmoth source code is available on GitHub_. The development version can be
+checked out via ``git`` using this command::
+
+  git clone https://github.com/jnikula/hawkmoth.git
+
+Please file bugs and feature requests as GitHub issues. Contributions are
+welcome both as emailed patches to the mailing list and as pull requests.
+
+.. _GitHub: https://github.com/jnikula/hawkmoth
+
+Dependencies
+------------
+
+- Python 3.4
+- Sphinx 1.8
+- Clang 6.0
+- Python 3 Bindings for Clang 6.0
+- sphinx-testing 1.0.0 (for development)
+
+These are the versions Hawkmoth is currently being developed and tested
+against. Other versions might work, but no guarantees.
+
+License
+-------
+
+Hawkmoth is free software, released under the `2-Clause BSD License`_.
+
+.. _2-Clause BSD License: https://opensource.org/licenses/BSD-2-Clause
+
+Contact
+-------
+
+IRC channel ``#hawkmoth`` on freenode_.
+
+Mailing list hawkmoth@freelists.org. Subscription information at the `list home
+page`_.
+
+.. _freenode: https://freenode.net/
+
+.. _list home page: https://www.freelists.org/list/hawkmoth
diff --git a/Documentation/hawkmoth/VERSION b/Documentation/hawkmoth/VERSION
new file mode 100644
index 000000000..bd73f4707
--- /dev/null
+++ b/Documentation/hawkmoth/VERSION
@@ -0,0 +1 @@
+0.4
diff --git a/Documentation/hawkmoth/__init__.py b/Documentation/hawkmoth/__init__.py
new file mode 100644
index 000000000..0896453ea
--- /dev/null
+++ b/Documentation/hawkmoth/__init__.py
@@ -0,0 +1,119 @@
+# Copyright (c) 2016-2017, Jani Nikula <jani@nikula.org>
+# Licensed under the terms of BSD 2-Clause, see LICENSE for details.
+"""
+Hawkmoth
+========
+
+Sphinx C Domain autodoc directive extension.
+"""
+
+import glob
+import os
+import re
+import stat
+import subprocess
+import sys
+
+from docutils import nodes, statemachine
+from docutils.parsers.rst import directives, Directive
+from docutils.statemachine import ViewList
+from sphinx.util.nodes import nested_parse_with_titles
+from sphinx.util.docutils import switch_source_input
+from sphinx.util import logging
+
+from hawkmoth.parser import parse, ErrorLevel
+
+with open(os.path.join(os.path.abspath(os.path.dirname(__file__)),
+                       'VERSION')) as version_file:
+    __version__ = version_file.read().strip()
+
+class CAutoDocDirective(Directive):
+    """Extract all documentation comments from the specified file"""
+    required_argument = 1
+    optional_arguments = 1
+    logger = logging.getLogger(__name__)
+
+    # Allow passing a variable number of file patterns as arguments
+    final_argument_whitespace = True
+
+    option_spec = {
+        'compat': directives.unchanged_required,
+        'clang': directives.unchanged_required,
+    }
+    has_content = False
+
+    # Map verbosity levels to logger levels.
+    _log_lvl = {ErrorLevel.ERROR: logging.LEVEL_NAMES['ERROR'],
+                ErrorLevel.WARNING: logging.LEVEL_NAMES['WARNING'],
+                ErrorLevel.INFO: logging.LEVEL_NAMES['INFO'],
+                ErrorLevel.DEBUG: logging.LEVEL_NAMES['DEBUG']}
+
+    def __display_parser_diagnostics(self, errors):
+        env = self.state.document.settings.env
+
+        for (severity, filename, lineno, msg) in errors:
+            toprint = '{}:{}: {}'.format(filename, lineno, msg)
+
+            if severity.value <= env.app.verbosity:
+                self.logger.log(self._log_lvl[severity], toprint,
+                                location=(env.docname, self.lineno))
+
+    def __parse(self, viewlist, filename):
+        env = self.state.document.settings.env
+
+        compat = self.options.get('compat', env.config.cautodoc_compat)
+        clang = self.options.get('clang', env.config.cautodoc_clang)
+
+        comments, errors = parse(filename, compat=compat, clang=clang)
+
+        self.__display_parser_diagnostics(errors)
+
+        for (comment, meta) in comments:
+            lineoffset = meta['line'] - 1
+            lines = statemachine.string2lines(comment, 8,
+                                              convert_whitespace=True)
+            for line in lines:
+                viewlist.append(line, filename, lineoffset)
+                lineoffset += 1
+
+    def run(self):
+        env = self.state.document.settings.env
+
+        result = ViewList()
+
+        for pattern in self.arguments[0].split():
+            filenames = glob.glob(env.config.cautodoc_root + '/' + pattern)
+            if len(filenames) == 0:
+                fmt = 'Pattern "{pat}" does not match any files.'
+                self.logger.warning(fmt.format(pat=pattern),
+                                    location=(env.docname, self.lineno))
+                continue
+
+            for filename in filenames:
+                mode = os.stat(filename).st_mode
+                if stat.S_ISDIR(mode):
+                    fmt = 'Path "{name}" matching pattern "{pat}" is a directory.'
+                    self.logger.warning(fmt.format(name=filename, pat=pattern),
+                                        location=(env.docname, self.lineno))
+                    continue
+
+                # Tell Sphinx about the dependency and parse the file
+                env.note_dependency(os.path.abspath(filename))
+                self.__parse(result, filename)
+
+        # Parse the extracted reST
+        with switch_source_input(self.state, result):
+            node = nodes.section()
+            nested_parse_with_titles(self.state, result, node)
+
+        return node.children
+
+def setup(app):
+    app.require_sphinx('1.8')
+    app.add_config_value('cautodoc_root', app.confdir, 'env')
+    app.add_config_value('cautodoc_compat', None, 'env')
+    app.add_config_value('cautodoc_clang', None, 'env')
+    app.add_directive_to_domain('c', 'autodoc', CAutoDocDirective)
+
+    return dict(version = __version__,
+                parallel_read_safe = True, parallel_write_safe = True)
diff --git a/Documentation/hawkmoth/__main__.py b/Documentation/hawkmoth/__main__.py
new file mode 100644
index 000000000..96f6ce7f2
--- /dev/null
+++ b/Documentation/hawkmoth/__main__.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2016-2019 Jani Nikula <jani@nikula.org>
+# Licensed under the terms of BSD 2-Clause, see LICENSE for details.
+"""
+Hawkmoth parser debug tool
+==========================
+
+python3 -m hawkmoth
+"""
+
+import argparse
+import sys
+
+from hawkmoth.parser import parse
+
+def main():
+    parser = argparse.ArgumentParser(prog='hawkmoth', description="""
+    Hawkmoth parser debug tool. Print the documentation comments extracted
+    from FILE, along with the generated C Domain directives, to standard
+    output. Include metadata with verbose output.""")
+    parser.add_argument('file', metavar='FILE', type=str, action='store',
+                        help='The C source or header file to parse.')
+    parser.add_argument('--compat',
+                        choices=['none',
+                                 'javadoc-basic',
+                                 'javadoc-liberal',
+                                 'kernel-doc'],
+                        help='Compatibility options. See cautodoc_compat.')
+    parser.add_argument('--clang', metavar='PARAM[,PARAM,...]',
+                        help='Arguments to pass to clang. See cautodoc_clang.')
+    parser.add_argument('--verbose', dest='verbose', action='store_true',
+                        help='Verbose output.')
+    args = parser.parse_args()
+
+    docs, errors = parse(args.file, compat=args.compat, clang=args.clang)
+
+    for (doc, meta) in docs:
+        if args.verbose:
+            print('# {}'.format(meta))
+        print(doc)
+
+    for (severity, filename, lineno, msg) in errors:
+        print('{}: {}:{}: {}'.format(severity.name,
+                                     filename, lineno, msg), file=sys.stderr)
+
+main()
diff --git a/Documentation/hawkmoth/parser.py b/Documentation/hawkmoth/parser.py
new file mode 100644
index 000000000..a070ad1cd
--- /dev/null
+++ b/Documentation/hawkmoth/parser.py
@@ -0,0 +1,301 @@
+# Copyright (c) 2016-2017 Jani Nikula <jani@nikula.org>
+# Copyright (c) 2018-2019 Bruno Santos <brunomanuelsantos@tecnico.ulisboa.pt>
+# Licensed under the terms of BSD 2-Clause, see LICENSE for details.
+"""
+Documentation comment extractor
+===============================
+
+This module extracts relevant documentation comments, optionally reformatting
+them in reST syntax.
+
+This is the part that uses Clang Python Bindings to extract documentation
+comments from C source code. This module does not depend on Sphinx.
+
+There are two passes:
+
+#. Pass over the tokens to find all the comments, including ones that aren't
+   attached to cursors.
+
+#. Pass over the cursors to document them.
+
+There is minimal syntax parsing or input conversion:
+
+* Identification of documentation comment blocks, and stripping the comment
+  delimiters (``/**`` and ``*/``) and continuation line prefixes (e.g. ``␣*␣``).
+
+* Identification of function-like macros.
+
+* Indentation for reST C Domain directive blocks.
+
+* An optional external filter may be invoked to support different syntaxes.
+  These filters are expected to translate the comment into the reST format.
+
+Otherwise, documentation comments are passed through verbatim.
+"""
+
+import enum
+import itertools
+import sys
+
+from clang.cindex import CursorKind, TypeKind
+from clang.cindex import Index, TranslationUnit
+from clang.cindex import SourceLocation, SourceRange
+from clang.cindex import TokenKind, TokenGroup
+
+from hawkmoth.util import docstr, doccompat
+
+class ErrorLevel(enum.Enum):
+    """
+    Supported error levels in inverse numerical order of severity. The values
+    are chosen so that they map directly to a 'verbosity level'.
+    """
+    ERROR = 0
+    WARNING = 1
+    INFO = 2
+    DEBUG = 3
+
+def comment_extract(tu):
+
+    # FIXME: How to handle top level comments above a cursor that it does *not*
+    # describe? Parsing @file or @doc at this stage would not be a clean design.
+    # One idea is to use '/***' to denote them, but that might throw off editor
+    # highlighting. The workaround is to follow the top level comment with an
+    # empty '/**/' comment that gets attached to the cursor.
+
+    top_level_comments = []
+    comments = {}
+    cursor = None
+    current_comment = None
+    for token in tu.get_tokens(extent=tu.cursor.extent):
+        # handle all comments we come across
+        if token.kind == TokenKind.COMMENT:
+            # if we already have a comment, it wasn't related to a cursor
+            if current_comment and docstr.is_doc(current_comment.spelling):
+                top_level_comments.append(current_comment)
+            current_comment = token
+            continue
+
+        # cursors that are 1) never documented themselves, and 2) allowed
+        # between comment and the actual cursor being documented
+        if (token.cursor.kind == CursorKind.INVALID_FILE or
+            token.cursor.kind == CursorKind.TYPE_REF or
+            token.cursor.kind == CursorKind.PREPROCESSING_DIRECTIVE or
+            token.cursor.kind == CursorKind.MACRO_INSTANTIATION):
+            continue
+
+        if cursor is not None and token.cursor == cursor:
+            continue
+
+        cursor = token.cursor
+
+        # Note: current_comment may be None
+        if current_comment != None and docstr.is_doc(current_comment.spelling):
+            comments[cursor.hash] = current_comment
+        current_comment = None
+
+    # comment at the end of file
+    if current_comment and docstr.is_doc(current_comment.spelling):
+        top_level_comments.append(current_comment)
+
+    return top_level_comments, comments
+
+def _result(comment, cursor=None, fmt=docstr.Type.TEXT, nest=0,
+            name=None, ttype=None, args=None, compat=None):
+
+    # FIXME: docstr.generate changes the number of lines in output. This impacts
+    # the error reporting via meta['line']. Adjust meta to take this into
+    # account.
+
+    doc = docstr.generate(text=comment.spelling, fmt=fmt,
+                          name=name, ttype=ttype, args=args, transform=compat)
+
+    doc = docstr.nest(doc, nest)
+
+    meta = {'line': comment.extent.start.line}
+    if cursor:
+        meta['cursor.kind']        = cursor.kind,
+        meta['cursor.displayname'] = cursor.displayname,
+        meta['cursor.spelling']    = cursor.spelling
+
+    return [(doc, meta)]
+
+# Return None for simple macros, a potentially empty list of arguments for
+# function-like macros
+def _get_macro_args(cursor):
+    if cursor.kind != CursorKind.MACRO_DEFINITION:
+        return None
+
+    # Use the first two tokens to make sure this starts with 'IDENTIFIER('
+    x = [token for token in itertools.islice(cursor.get_tokens(), 2)]
+    if (len(x) != 2 or x[0].spelling != cursor.spelling or
+        x[1].spelling != '(' or x[0].extent.end != x[1].extent.start):
+        return None
+
+    # Naïve parsing of macro arguments
+    # FIXME: This doesn't handle GCC named vararg extension FOO(vararg...)
+    args = []
+    for token in itertools.islice(cursor.get_tokens(), 2, None):
+        if token.spelling == ')':
+            return args
+        elif token.spelling == ',':
+            continue
+        elif token.kind == TokenKind.IDENTIFIER:
+            args.append(token.spelling)
+        elif token.spelling == '...':
+            args.append(token.spelling)
+        else:
+            break
+
+    return None
+
+def _recursive_parse(comments, cursor, nest, compat):
+    comment = comments[cursor.hash]
+    name = cursor.spelling
+    ttype = cursor.type.spelling
+
+    if cursor.kind == CursorKind.MACRO_DEFINITION:
+        # FIXME: check args against comment
+        args = _get_macro_args(cursor)
+        fmt = docstr.Type.MACRO if args is None else docstr.Type.MACRO_FUNC
+
+        return _result(comment, cursor=cursor, fmt=fmt,
+                       nest=nest, name=name, args=args, compat=compat)
+
+    elif cursor.kind == CursorKind.VAR_DECL:
+        fmt = docstr.Type.VAR
+
+        return _result(comment, cursor=cursor, fmt=fmt,
+                       nest=nest, name=name, ttype=ttype, compat=compat)
+
+    elif cursor.kind == CursorKind.TYPEDEF_DECL:
+        # FIXME: function pointers typedefs.
+        fmt = docstr.Type.TYPE
+
+        return _result(comment, cursor=cursor, fmt=fmt,
+                       nest=nest, name=ttype, compat=compat)
+
+    elif cursor.kind in [CursorKind.STRUCT_DECL, CursorKind.UNION_DECL,
+                         CursorKind.ENUM_DECL]:
+
+        # FIXME:
+        # Handle cases where variables are instantiated on type declaration,
+        # including anonymous cases. Idea is that if there is a variable
+        # instantiation, the documentation should be applied to the variable if
+        # the structure is anonymous or to the type otherwise.
+        #
+        # Due to the new recursiveness of the parser, fixing this here, _should_
+        # handle all cases (struct, union, enum).
+
+        # FIXME: Handle anonymous enumerators.
+
+        fmt = docstr.Type.TYPE
+        result = _result(comment, cursor=cursor, fmt=fmt,
+                         nest=nest, name=ttype, compat=compat)
+
+        nest += 1
+        for c in cursor.get_children():
+            if c.hash in comments:
+                result.extend(_recursive_parse(comments, c, nest, compat))
+
+        return result
+
+    elif cursor.kind == CursorKind.ENUM_CONSTANT_DECL:
+        fmt = docstr.Type.ENUM_VAL
+
+        return _result(comment, cursor=cursor, fmt=fmt,
+                       nest=nest, name=name, compat=compat)
+
+    elif cursor.kind == CursorKind.FIELD_DECL:
+        fmt = docstr.Type.MEMBER
+
+        return _result(comment, cursor=cursor, fmt=fmt,
+                       nest=nest, name=name, ttype=ttype, compat=compat)
+
+    elif cursor.kind == CursorKind.FUNCTION_DECL:
+        # FIXME: check args against comment
+        # FIXME: children may contain extra stuff if the return type is a
+        # typedef, for example
+        args = []
+
+        # Only fully prototyped functions will have argument lists to process.
+        if cursor.type.kind == TypeKind.FUNCTIONPROTO:
+            for c in cursor.get_children():
+                if c.kind == CursorKind.PARM_DECL:
+                    args.append('{ttype} {arg}'.format(ttype=c.type.spelling,
+                                                    arg=c.spelling))
+
+            if cursor.type.is_function_variadic():
+                args.append('...')
+
+        fmt = docstr.Type.FUNC
+        ttype = cursor.result_type.spelling
+
+        return _result(comment, cursor=cursor, fmt=fmt, nest=nest,
+                       name=name, ttype=ttype, args=args, compat=compat)
+
+    # FIXME: If we reach here, nothing matched. This is a warning or even error
+    # and it should be logged, but it should also return an empty list so that
+    # it doesn't break. I.e. the parser needs to pass warnings and errors to the
+    # Sphinx extension instead of polluting the generated output.
+    fmt = docstr.Type.TEXT
+    text = 'warning: unhandled cursor {kind} {name}\n'.format(
+        kind=str(cursor.kind),
+        name=cursor.spelling)
+
+    doc = docstr.generate(text=text, fmt=fmt)
+
+    meta = {
+        'line':               comment.extent.start.line,
+        'cursor.kind':        cursor.kind,
+        'cursor.displayname': cursor.displayname,
+        'cursor.spelling':    cursor.spelling
+    }
+
+    return [(doc, meta)]
+
+def clang_diagnostics(errors, diagnostics):
+    sev = {0: ErrorLevel.DEBUG,
+           1: ErrorLevel.DEBUG,
+           2: ErrorLevel.WARNING,
+           3: ErrorLevel.ERROR,
+           4: ErrorLevel.ERROR}
+
+    for diag in diagnostics:
+        errors.extend([(sev[diag.severity], diag.location.file.name,
+                        diag.location.line, diag.spelling)])
+
+# return a list of (comment, metadata) tuples
+# options - dictionary with directive options
+def parse(filename, **options):
+
+    errors = []
+    args = options.get('clang')
+    if args is not None:
+        args = [s.strip() for s in args.split(',') if len(s.strip()) > 0]
+        if len(args) == 0:
+            args = None
+
+    index = Index.create()
+
+    tu = index.parse(filename, args=args, options=
+                     TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
+                     TranslationUnit.PARSE_SKIP_FUNCTION_BODIES)
+
+    clang_diagnostics(errors, tu.diagnostics)
+
+    top_level_comments, comments = comment_extract(tu)
+
+    result = []
+    compat = lambda x: doccompat.convert(x, options.get('compat'))
+
+    for comment in top_level_comments:
+        result.extend(_result(comment, compat=compat))
+
+    for cursor in tu.cursor.get_children():
+        if cursor.hash in comments:
+            result.extend(_recursive_parse(comments, cursor, 0, compat))
+
+    # Sort all elements by order of appearance.
+    result.sort(key=lambda r: r[1]['line'])
+
+    return result, errors
diff --git a/Documentation/hawkmoth/util/__init__.py b/Documentation/hawkmoth/util/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Documentation/hawkmoth/util/doccompat.py b/Documentation/hawkmoth/util/doccompat.py
new file mode 100644
index 000000000..e687bc4b4
--- /dev/null
+++ b/Documentation/hawkmoth/util/doccompat.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2016-2017, Jani Nikula <jani@nikula.org>
+# Licensed under the terms of BSD 2-Clause, see LICENSE for details.
+"""
+Alternative docstring syntax
+============================
+
+This module abstracts different compatibility options converting different
+syntaxes into 'native' reST ones.
+"""
+
+import re
+
+# Basic Javadoc/Doxygen/kernel-doc import
+#
+# FIXME: One of the design goals of Hawkmoth is to keep things simple. There's a
+# fine balance between sticking to that goal and adding compat code to
+# facilitate any kind of migration to Hawkmoth. The compat code could be turned
+# into a fairly simple plugin architecture, with some basic compat builtins, and
+# the users could still extend the compat features to fit their specific needs.
+def convert(comment, mode):
+    """Convert documentation from a supported syntax into reST."""
+    # FIXME: try to preserve whitespace better
+
+    if mode == 'javadoc-basic' or mode == 'javadoc-liberal':
+        # @param
+        comment = re.sub(r"(?m)^([ \t]*)@param([ \t]+)([a-zA-Z0-9_]+|\.\.\.)([ \t]+)",
+                         "\n\\1:param\\2\\3:\\4", comment)
+        # @param[direction]
+        comment = re.sub(r"(?m)^([ \t]*)@param\[([^]]*)\]([ \t]+)([a-zA-Z0-9_]+|\.\.\.)([ \t]+)",
+                         "\n\\1:param\\3\\4: *(\\2)* \\5", comment)
+        # @return
+        comment = re.sub(r"(?m)^([ \t]*)@returns?([ \t]+|$)",
+                         "\n\\1:return:\\2", comment)
+        # @code/@endcode blocks. Works if the code is indented.
+        comment = re.sub(r"(?m)^([ \t]*)@code([ \t]+|$)",
+                         "\n::\n", comment)
+        comment = re.sub(r"(?m)^([ \t]*)@endcode([ \t]+|$)",
+                         "\n", comment)
+        # Ignore @brief.
+        comment = re.sub(r"(?m)^([ \t]*)@brief[ \t]+", "\n\\1", comment)
+
+        # Ignore groups
+        comment = re.sub(r"(?m)^([ \t]*)@(defgroup|addtogroup)[ \t]+[a-zA-Z0-9_]+[ \t]*",
+                         "\n\\1", comment)
+        comment = re.sub(r"(?m)^([ \t]*)@(ingroup|{|}).*", "\n", comment)
+
+    if mode == 'javadoc-liberal':
+        # Liberal conversion of any @tags, will fail for @code etc. but don't
+        # care.
+        comment = re.sub(r"(?m)^([ \t]*)@([a-zA-Z0-9_]+)([ \t]+)",
+                         "\n\\1:\\2:\\3", comment)
+
+    if mode == 'kernel-doc':
+        # Basic kernel-doc convert, will document struct members as params, etc.
+        comment = re.sub(r"(?m)^([ \t]*)@(returns?|RETURNS?):([ \t]+|$)",
+                         "\n\\1:return:\\3", comment)
+        comment = re.sub(r"(?m)^([ \t]*)@([a-zA-Z0-9_]+|\.\.\.):([ \t]+)",
+                         "\n\\1:param \\2:\\3", comment)
+
+    return comment
diff --git a/Documentation/hawkmoth/util/docstr.py b/Documentation/hawkmoth/util/docstr.py
new file mode 100644
index 000000000..2e6572f92
--- /dev/null
+++ b/Documentation/hawkmoth/util/docstr.py
@@ -0,0 +1,101 @@
+# Copyright (c) 2016-2017 Jani Nikula <jani@nikula.org>
+# Copyright (c) 2018-2019 Bruno Santos <brunomanuelsantos@tecnico.ulisboa.pt>
+# Licensed under the terms of BSD 2-Clause, see LICENSE for details.
+"""
+Documentation strings manipulation library
+==========================================
+
+This module allows a generic way of generating reST documentation for each C
+construct.
+"""
+
+import re
+from enum import Enum, auto
+
+class Type(Enum):
+    """Enumeration of supported formats."""
+    TEXT = auto()
+    VAR = auto()
+    TYPE = auto()
+    ENUM_VAL = auto()
+    MEMBER = auto()
+    MACRO = auto()
+    MACRO_FUNC = auto()
+    FUNC = auto()
+
+# Dictionary of tuples (text indentation level, format string).
+#
+# Text indentation is required for indenting the documentation body relative to
+# directive lines.
+_doc_fmt = {
+    Type.TEXT:       (0, '\n{text}\n'),
+    Type.VAR:        (1, '\n.. c:var:: {ttype} {name}\n\n{text}\n'),
+    Type.TYPE:       (1, '\n.. c:type:: {name}\n\n{text}\n'),
+    Type.ENUM_VAL:   (1, '\n.. c:macro:: {name}\n\n{text}\n'),
+    Type.MEMBER:     (1, '\n.. c:member:: {ttype} {name}\n\n{text}\n'),
+    Type.MACRO:      (1, '\n.. c:macro:: {name}\n\n{text}\n'),
+    Type.MACRO_FUNC: (1, '\n.. c:function:: {name}({args})\n\n{text}\n'),
+    Type.FUNC:       (1, '\n.. c:function:: {ttype} {name}({args})\n\n{text}\n')
+}
+
+def _strip(comment):
+    """Strip comment from comment markers."""
+    comment = re.sub(r'^/\*\*[ \t]?', '', comment)
+    comment = re.sub(r'\*/$', '', comment)
+    # Could look at first line of comment, and remove the leading stuff there
+    # from the rest.
+    comment = re.sub(r'(?m)^[ \t]*\*?[ \t]?', '', comment)
+    # Strip leading blank lines.
+    comment = re.sub(r'^[\n]*', '', comment)
+    return comment.strip()
+
+def is_doc(comment):
+    """Test if comment is a C documentation comment."""
+    return comment.startswith('/**') and comment != '/**/'
+
+def nest(text, nest):
+    """
+    Indent documentation block for nesting.
+
+    Args:
+        text (str): Documentation body.
+        nest (int): Nesting level. For each level, the final block is indented
+            one level. Useful for (e.g.) declaring structure members.
+
+    Returns:
+        str: Indented reST documentation string.
+    """
+    return re.sub('(?m)^(?!$)', '   ' * nest, text)
+
+def generate(text, fmt=Type.TEXT, name=None,
+             ttype=None, args=None, transform=None):
+    """
+    Generate reST documentation string.
+
+    Args:
+        text (str): Documentation body.
+        fmt (enum :py:class:`Type`): Format type to use. Different formats
+            require different arguments and ignores others if given.
+        name (str): Name of the documented token.
+        ttype (str): Type of the documented token.
+        args (list): List of arguments (str).
+        transform (func): Transformation function to be applied to the
+            documentation body. This is useful (e.g.) to extend the generator
+            with different syntaxes by converting them to reST. This is applied
+            on the documentation body after removing comment markers.
+
+    Returns:
+        str: reST documentation string.
+    """
+
+    text = _strip(text)
+
+    if transform:
+        text = transform(text)
+
+    if args is not None:
+        args = ', '.join(args)
+
+    (text_indent, fmt) = _doc_fmt[fmt]
+    text = nest(text, text_indent)
+    return fmt.format(text=text, name=name, ttype=ttype, args=args)
-- 
GitLab