| # 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 two | |
| # states: reading the C code fragment, or inside a rST fragment. | |
| class HxState(Enum): | |
| CTEXT = 1 | |
| RST = 2 | |
| 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(r'\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. We strip out any trailing ':' for | |
| # consistency with other headings in the rST documentation. | |
| 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. We strip out any trailing ':' for | |
| # consistency with other headings in the rST documentation. | |
| match = re.match(r'ARCHHEADING\((.*?):?,.*\)', line) | |
| if match is None: | |
| serror(file, lnum, "Invalid ARCHHEADING line") | |
| return match.group(1) | |
| def parse_srst(file, lnum, line): | |
| """Handle an SRST directive""" | |
| # The input should be either "SRST", or "SRST(label)". | |
| match = re.match(r'SRST(\((.*?)\))?', line) | |
| if match is None: | |
| serror(file, lnum, "Invalid SRST line") | |
| return match.group(2) | |
| 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 == 'SRST': | |
| if state == HxState.RST: | |
| serror(hxfile, lnum, 'expected ERST, found SRST') | |
| else: | |
| state = HxState.RST | |
| label = parse_srst(hxfile, lnum, line) | |
| if label: | |
| rstlist.append("", hxfile, lnum - 1) | |
| # Build label as _DOCNAME-HXNAME-LABEL | |
| hx = os.path.splitext(os.path.basename(hxfile))[0] | |
| refline = ".. _" + env.docname + "-" + hx + \ | |
| "-" + label + ":" | |
| rstlist.append(refline, hxfile, lnum - 1) | |
| elif directive == 'ERST': | |
| if 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 | |
| ) |