--- /dev/null
+# coding=utf-8
+#
+# QEMU hxtool .hx file parsing extension
+#
+# Copyright (c) 2020 Linaro
+#
+# This work is licensed under the terms of the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+"""hxtool is a Sphinx extension that implements the hxtool-doc directive"""
+
+# The purpose of this extension is to read fragments of rST
+# from .hx files, and insert them all into the current document.
+# The rST fragments are delimited by SRST/ERST lines.
+# The conf.py file must set the hxtool_srctree config value to
+# the root of the QEMU source tree.
+# Each hxtool-doc:: directive takes one argument which is the
+# path of the .hx file to process, relative to the source tree.
+
+import os
+import re
+from enum import Enum
+
+from docutils import nodes
+from docutils.statemachine import ViewList
+from docutils.parsers.rst import directives, Directive
+from sphinx.errors import ExtensionError
+from sphinx.util.nodes import nested_parse_with_titles
+import sphinx
+
+# Sphinx up to 1.6 uses AutodocReporter; 1.7 and later
+# use switch_source_input. Check borrowed from kerneldoc.py.
+Use_SSI = sphinx.__version__[:3] >= '1.7'
+if Use_SSI:
+ from sphinx.util.docutils import switch_source_input
+else:
+ from sphinx.ext.autodoc import AutodocReporter
+
+__version__ = '1.0'
+
+# We parse hx files with a state machine which may be in one of three
+# states: reading the C code fragment, inside a texi fragment,
+# or inside a rST fragment.
+class HxState(Enum):
+ CTEXT = 1
+ TEXI = 2
+ RST = 3
+
+def serror(file, lnum, errtext):
+ """Raise an exception giving a user-friendly syntax error message"""
+ raise ExtensionError('%s line %d: syntax error: %s' % (file, lnum, errtext))
+
+def parse_directive(line):
+ """Return first word of line, if any"""
+ return re.split('\W', line)[0]
+
+def parse_defheading(file, lnum, line):
+ """Handle a DEFHEADING directive"""
+ # The input should be "DEFHEADING(some string)", though note that
+ # the 'some string' could be the empty string. If the string is
+ # empty we ignore the directive -- these are used only to add
+ # blank lines in the plain-text content of the --help output.
+ #
+ # Return the heading text
+ match = re.match(r'DEFHEADING\((.*)\)', line)
+ if match is None:
+ serror(file, lnum, "Invalid DEFHEADING line")
+ return match.group(1)
+
+def parse_archheading(file, lnum, line):
+ """Handle an ARCHHEADING directive"""
+ # The input should be "ARCHHEADING(some string, other arg)",
+ # though note that the 'some string' could be the empty string.
+ # As with DEFHEADING, empty string ARCHHEADINGs will be ignored.
+ #
+ # Return the heading text
+ match = re.match(r'ARCHHEADING\((.*),.*\)', line)
+ if match is None:
+ serror(file, lnum, "Invalid ARCHHEADING line")
+ return match.group(1)
+
+class HxtoolDocDirective(Directive):
+ """Extract rST fragments from the specified .hx file"""
+ required_argument = 1
+ optional_arguments = 1
+ option_spec = {
+ 'hxfile': directives.unchanged_required
+ }
+ has_content = False
+
+ def run(self):
+ env = self.state.document.settings.env
+ hxfile = env.config.hxtool_srctree + '/' + self.arguments[0]
+
+ # Tell sphinx of the dependency
+ env.note_dependency(os.path.abspath(hxfile))
+
+ state = HxState.CTEXT
+ # We build up lines of rST in this ViewList, which we will
+ # later put into a 'section' node.
+ rstlist = ViewList()
+ current_node = None
+ node_list = []
+
+ with open(hxfile) as f:
+ lines = (l.rstrip() for l in f)
+ for lnum, line in enumerate(lines, 1):
+ directive = parse_directive(line)
+
+ if directive == 'HXCOMM':
+ pass
+ elif directive == 'STEXI':
+ if state == HxState.RST:
+ serror(hxfile, lnum, 'expected ERST, found STEXI')
+ elif state == HxState.TEXI:
+ serror(hxfile, lnum, 'expected ETEXI, found STEXI')
+ else:
+ state = HxState.TEXI
+ elif directive == 'ETEXI':
+ if state == HxState.RST:
+ serror(hxfile, lnum, 'expected ERST, found ETEXI')
+ elif state == HxState.CTEXT:
+ serror(hxfile, lnum, 'expected STEXI, found ETEXI')
+ else:
+ state = HxState.CTEXT
+ elif directive == 'SRST':
+ if state == HxState.RST:
+ serror(hxfile, lnum, 'expected ERST, found SRST')
+ elif state == HxState.TEXI:
+ serror(hxfile, lnum, 'expected ETEXI, found SRST')
+ else:
+ state = HxState.RST
+ elif directive == 'ERST':
+ if state == HxState.TEXI:
+ serror(hxfile, lnum, 'expected ETEXI, found ERST')
+ elif state == HxState.CTEXT:
+ serror(hxfile, lnum, 'expected SRST, found ERST')
+ else:
+ state = HxState.CTEXT
+ elif directive == 'DEFHEADING' or directive == 'ARCHHEADING':
+ if directive == 'DEFHEADING':
+ heading = parse_defheading(hxfile, lnum, line)
+ else:
+ heading = parse_archheading(hxfile, lnum, line)
+ if heading == "":
+ continue
+ # Put the accumulated rST into the previous node,
+ # and then start a fresh section with this heading.
+ if len(rstlist) > 0:
+ if current_node is None:
+ # We had some rST fragments before the first
+ # DEFHEADING. We don't have a section to put
+ # these in, so rather than magicing up a section,
+ # make it a syntax error.
+ serror(hxfile, lnum,
+ 'first DEFHEADING must precede all rST text')
+ self.do_parse(rstlist, current_node)
+ rstlist = ViewList()
+ if current_node is not None:
+ node_list.append(current_node)
+ section_id = 'hxtool-%d' % env.new_serialno('hxtool')
+ current_node = nodes.section(ids=[section_id])
+ current_node += nodes.title(heading, heading)
+ else:
+ # Not a directive: put in output if we are in rST fragment
+ if state == HxState.RST:
+ # Sphinx counts its lines from 0
+ rstlist.append(line, hxfile, lnum - 1)
+
+ if current_node is None:
+ # We don't have multiple sections, so just parse the rst
+ # fragments into a dummy node so we can return the children.
+ current_node = nodes.section()
+ self.do_parse(rstlist, current_node)
+ return current_node.children
+ else:
+ # Put the remaining accumulated rST into the last section, and
+ # return all the sections.
+ if len(rstlist) > 0:
+ self.do_parse(rstlist, current_node)
+ node_list.append(current_node)
+ return node_list
+
+ # This is from kerneldoc.py -- it works around an API change in
+ # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use
+ # sphinx.util.nodes.nested_parse_with_titles() rather than the
+ # plain self.state.nested_parse(), and so we can drop the saving
+ # of title_styles and section_level that kerneldoc.py does,
+ # because nested_parse_with_titles() does that for us.
+ def do_parse(self, result, node):
+ if Use_SSI:
+ with switch_source_input(self.state, result):
+ nested_parse_with_titles(self.state, result, node)
+ else:
+ save = self.state.memo.reporter
+ self.state.memo.reporter = AutodocReporter(result, self.state.memo.reporter)
+ try:
+ nested_parse_with_titles(self.state, result, node)
+ finally:
+ self.state.memo.reporter = save
+
+def setup(app):
+ """ Register hxtool-doc directive with Sphinx"""
+ app.add_config_value('hxtool_srctree', None, 'env')
+ app.add_directive('hxtool-doc', HxtoolDocDirective)
+
+ return dict(
+ version = __version__,
+ parallel_read_safe = True,
+ parallel_write_safe = True
+ )