xref: /openbmc/qemu/docs/sphinx/hxtool.py (revision 7d87775f)
1# coding=utf-8
2#
3# QEMU hxtool .hx file parsing extension
4#
5# Copyright (c) 2020 Linaro
6#
7# This work is licensed under the terms of the GNU GPLv2 or later.
8# See the COPYING file in the top-level directory.
9"""hxtool is a Sphinx extension that implements the hxtool-doc directive"""
10
11# The purpose of this extension is to read fragments of rST
12# from .hx files, and insert them all into the current document.
13# The rST fragments are delimited by SRST/ERST lines.
14# The conf.py file must set the hxtool_srctree config value to
15# the root of the QEMU source tree.
16# Each hxtool-doc:: directive takes one argument which is the
17# path of the .hx file to process, relative to the source tree.
18
19import os
20import re
21from enum import Enum
22
23from docutils import nodes
24from docutils.statemachine import ViewList
25from docutils.parsers.rst import directives, Directive
26from sphinx.errors import ExtensionError
27from sphinx.util.docutils import switch_source_input
28from sphinx.util.nodes import nested_parse_with_titles
29import sphinx
30
31
32__version__ = '1.0'
33
34# We parse hx files with a state machine which may be in one of two
35# states: reading the C code fragment, or inside a rST fragment.
36class HxState(Enum):
37    CTEXT = 1
38    RST = 2
39
40def serror(file, lnum, errtext):
41    """Raise an exception giving a user-friendly syntax error message"""
42    raise ExtensionError('%s line %d: syntax error: %s' % (file, lnum, errtext))
43
44def parse_directive(line):
45    """Return first word of line, if any"""
46    return re.split(r'\W', line)[0]
47
48def parse_defheading(file, lnum, line):
49    """Handle a DEFHEADING directive"""
50    # The input should be "DEFHEADING(some string)", though note that
51    # the 'some string' could be the empty string. If the string is
52    # empty we ignore the directive -- these are used only to add
53    # blank lines in the plain-text content of the --help output.
54    #
55    # Return the heading text. We strip out any trailing ':' for
56    # consistency with other headings in the rST documentation.
57    match = re.match(r'DEFHEADING\((.*?):?\)', line)
58    if match is None:
59        serror(file, lnum, "Invalid DEFHEADING line")
60    return match.group(1)
61
62def parse_archheading(file, lnum, line):
63    """Handle an ARCHHEADING directive"""
64    # The input should be "ARCHHEADING(some string, other arg)",
65    # though note that the 'some string' could be the empty string.
66    # As with DEFHEADING, empty string ARCHHEADINGs will be ignored.
67    #
68    # Return the heading text. We strip out any trailing ':' for
69    # consistency with other headings in the rST documentation.
70    match = re.match(r'ARCHHEADING\((.*?):?,.*\)', line)
71    if match is None:
72        serror(file, lnum, "Invalid ARCHHEADING line")
73    return match.group(1)
74
75def parse_srst(file, lnum, line):
76    """Handle an SRST directive"""
77    # The input should be either "SRST", or "SRST(label)".
78    match = re.match(r'SRST(\((.*?)\))?', line)
79    if match is None:
80        serror(file, lnum, "Invalid SRST line")
81    return match.group(2)
82
83class HxtoolDocDirective(Directive):
84    """Extract rST fragments from the specified .hx file"""
85    required_argument = 1
86    optional_arguments = 1
87    option_spec = {
88        'hxfile': directives.unchanged_required
89    }
90    has_content = False
91
92    def run(self):
93        env = self.state.document.settings.env
94        hxfile = env.config.hxtool_srctree + '/' + self.arguments[0]
95
96        # Tell sphinx of the dependency
97        env.note_dependency(os.path.abspath(hxfile))
98
99        state = HxState.CTEXT
100        # We build up lines of rST in this ViewList, which we will
101        # later put into a 'section' node.
102        rstlist = ViewList()
103        current_node = None
104        node_list = []
105
106        with open(hxfile) as f:
107            lines = (l.rstrip() for l in f)
108            for lnum, line in enumerate(lines, 1):
109                directive = parse_directive(line)
110
111                if directive == 'HXCOMM':
112                    pass
113                elif directive == 'SRST':
114                    if state == HxState.RST:
115                        serror(hxfile, lnum, 'expected ERST, found SRST')
116                    else:
117                        state = HxState.RST
118                        label = parse_srst(hxfile, lnum, line)
119                        if label:
120                            rstlist.append("", hxfile, lnum - 1)
121                            # Build label as _DOCNAME-HXNAME-LABEL
122                            hx = os.path.splitext(os.path.basename(hxfile))[0]
123                            refline = ".. _" + env.docname + "-" + hx + \
124                                "-" + label + ":"
125                            rstlist.append(refline, hxfile, lnum - 1)
126                elif directive == 'ERST':
127                    if state == HxState.CTEXT:
128                        serror(hxfile, lnum, 'expected SRST, found ERST')
129                    else:
130                        state = HxState.CTEXT
131                elif directive == 'DEFHEADING' or directive == 'ARCHHEADING':
132                    if directive == 'DEFHEADING':
133                        heading = parse_defheading(hxfile, lnum, line)
134                    else:
135                        heading = parse_archheading(hxfile, lnum, line)
136                    if heading == "":
137                        continue
138                    # Put the accumulated rST into the previous node,
139                    # and then start a fresh section with this heading.
140                    if len(rstlist) > 0:
141                        if current_node is None:
142                            # We had some rST fragments before the first
143                            # DEFHEADING. We don't have a section to put
144                            # these in, so rather than magicing up a section,
145                            # make it a syntax error.
146                            serror(hxfile, lnum,
147                                   'first DEFHEADING must precede all rST text')
148                        self.do_parse(rstlist, current_node)
149                        rstlist = ViewList()
150                    if current_node is not None:
151                        node_list.append(current_node)
152                    section_id = 'hxtool-%d' % env.new_serialno('hxtool')
153                    current_node = nodes.section(ids=[section_id])
154                    current_node += nodes.title(heading, heading)
155                else:
156                    # Not a directive: put in output if we are in rST fragment
157                    if state == HxState.RST:
158                        # Sphinx counts its lines from 0
159                        rstlist.append(line, hxfile, lnum - 1)
160
161        if current_node is None:
162            # We don't have multiple sections, so just parse the rst
163            # fragments into a dummy node so we can return the children.
164            current_node = nodes.section()
165            self.do_parse(rstlist, current_node)
166            return current_node.children
167        else:
168            # Put the remaining accumulated rST into the last section, and
169            # return all the sections.
170            if len(rstlist) > 0:
171                self.do_parse(rstlist, current_node)
172            node_list.append(current_node)
173            return node_list
174
175    # This is from kerneldoc.py -- it works around an API change in
176    # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use
177    # sphinx.util.nodes.nested_parse_with_titles() rather than the
178    # plain self.state.nested_parse(), and so we can drop the saving
179    # of title_styles and section_level that kerneldoc.py does,
180    # because nested_parse_with_titles() does that for us.
181    def do_parse(self, result, node):
182        with switch_source_input(self.state, result):
183            nested_parse_with_titles(self.state, result, node)
184
185
186def setup(app):
187    """ Register hxtool-doc directive with Sphinx"""
188    app.add_config_value('hxtool_srctree', None, 'env')
189    app.add_directive('hxtool-doc', HxtoolDocDirective)
190
191    return dict(
192        version = __version__,
193        parallel_read_safe = True,
194        parallel_write_safe = True
195    )
196