xref: /openbmc/qemu/docs/sphinx/hxtool.py (revision d0c832f616cadd5bace44986824c0c9142913795)
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.nodes import nested_parse_with_titles
28import sphinx
29
30# Sphinx up to 1.6 uses AutodocReporter; 1.7 and later
31# use switch_source_input. Check borrowed from kerneldoc.py.
32Use_SSI = sphinx.__version__[:3] >= '1.7'
33if Use_SSI:
34    from sphinx.util.docutils import switch_source_input
35else:
36    from sphinx.ext.autodoc import AutodocReporter
37
38__version__ = '1.0'
39
40# We parse hx files with a state machine which may be in one of three
41# states: reading the C code fragment, inside a texi fragment,
42# or inside a rST fragment.
43class HxState(Enum):
44    CTEXT = 1
45    TEXI = 2
46    RST = 3
47
48def serror(file, lnum, errtext):
49    """Raise an exception giving a user-friendly syntax error message"""
50    raise ExtensionError('%s line %d: syntax error: %s' % (file, lnum, errtext))
51
52def parse_directive(line):
53    """Return first word of line, if any"""
54    return re.split('\W', line)[0]
55
56def parse_defheading(file, lnum, line):
57    """Handle a DEFHEADING directive"""
58    # The input should be "DEFHEADING(some string)", though note that
59    # the 'some string' could be the empty string. If the string is
60    # empty we ignore the directive -- these are used only to add
61    # blank lines in the plain-text content of the --help output.
62    #
63    # Return the heading text. We strip out any trailing ':' for
64    # consistency with other headings in the rST documentation.
65    match = re.match(r'DEFHEADING\((.*?):?\)', line)
66    if match is None:
67        serror(file, lnum, "Invalid DEFHEADING line")
68    return match.group(1)
69
70def parse_archheading(file, lnum, line):
71    """Handle an ARCHHEADING directive"""
72    # The input should be "ARCHHEADING(some string, other arg)",
73    # though note that the 'some string' could be the empty string.
74    # As with DEFHEADING, empty string ARCHHEADINGs will be ignored.
75    #
76    # Return the heading text. We strip out any trailing ':' for
77    # consistency with other headings in the rST documentation.
78    match = re.match(r'ARCHHEADING\((.*?):?,.*\)', line)
79    if match is None:
80        serror(file, lnum, "Invalid ARCHHEADING line")
81    return match.group(1)
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 == 'STEXI':
114                    if state == HxState.RST:
115                        serror(hxfile, lnum, 'expected ERST, found STEXI')
116                    elif state == HxState.TEXI:
117                        serror(hxfile, lnum, 'expected ETEXI, found STEXI')
118                    else:
119                        state = HxState.TEXI
120                elif directive == 'ETEXI':
121                    if state == HxState.RST:
122                        serror(hxfile, lnum, 'expected ERST, found ETEXI')
123                    elif state == HxState.CTEXT:
124                        serror(hxfile, lnum, 'expected STEXI, found ETEXI')
125                    else:
126                        state = HxState.CTEXT
127                elif directive == 'SRST':
128                    if state == HxState.RST:
129                        serror(hxfile, lnum, 'expected ERST, found SRST')
130                    elif state == HxState.TEXI:
131                        serror(hxfile, lnum, 'expected ETEXI, found SRST')
132                    else:
133                        state = HxState.RST
134                elif directive == 'ERST':
135                    if state == HxState.TEXI:
136                        serror(hxfile, lnum, 'expected ETEXI, found ERST')
137                    elif state == HxState.CTEXT:
138                        serror(hxfile, lnum, 'expected SRST, found ERST')
139                    else:
140                        state = HxState.CTEXT
141                elif directive == 'DEFHEADING' or directive == 'ARCHHEADING':
142                    if directive == 'DEFHEADING':
143                        heading = parse_defheading(hxfile, lnum, line)
144                    else:
145                        heading = parse_archheading(hxfile, lnum, line)
146                    if heading == "":
147                        continue
148                    # Put the accumulated rST into the previous node,
149                    # and then start a fresh section with this heading.
150                    if len(rstlist) > 0:
151                        if current_node is None:
152                            # We had some rST fragments before the first
153                            # DEFHEADING. We don't have a section to put
154                            # these in, so rather than magicing up a section,
155                            # make it a syntax error.
156                            serror(hxfile, lnum,
157                                   'first DEFHEADING must precede all rST text')
158                        self.do_parse(rstlist, current_node)
159                        rstlist = ViewList()
160                    if current_node is not None:
161                        node_list.append(current_node)
162                    section_id = 'hxtool-%d' % env.new_serialno('hxtool')
163                    current_node = nodes.section(ids=[section_id])
164                    current_node += nodes.title(heading, heading)
165                else:
166                    # Not a directive: put in output if we are in rST fragment
167                    if state == HxState.RST:
168                        # Sphinx counts its lines from 0
169                        rstlist.append(line, hxfile, lnum - 1)
170
171        if current_node is None:
172            # We don't have multiple sections, so just parse the rst
173            # fragments into a dummy node so we can return the children.
174            current_node = nodes.section()
175            self.do_parse(rstlist, current_node)
176            return current_node.children
177        else:
178            # Put the remaining accumulated rST into the last section, and
179            # return all the sections.
180            if len(rstlist) > 0:
181                self.do_parse(rstlist, current_node)
182            node_list.append(current_node)
183            return node_list
184
185    # This is from kerneldoc.py -- it works around an API change in
186    # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use
187    # sphinx.util.nodes.nested_parse_with_titles() rather than the
188    # plain self.state.nested_parse(), and so we can drop the saving
189    # of title_styles and section_level that kerneldoc.py does,
190    # because nested_parse_with_titles() does that for us.
191    def do_parse(self, result, node):
192        if Use_SSI:
193            with switch_source_input(self.state, result):
194                nested_parse_with_titles(self.state, result, node)
195        else:
196            save = self.state.memo.reporter
197            self.state.memo.reporter = AutodocReporter(result, self.state.memo.reporter)
198            try:
199                nested_parse_with_titles(self.state, result, node)
200            finally:
201                self.state.memo.reporter = save
202
203def setup(app):
204    """ Register hxtool-doc directive with Sphinx"""
205    app.add_config_value('hxtool_srctree', None, 'env')
206    app.add_directive('hxtool-doc', HxtoolDocDirective)
207
208    return dict(
209        version = __version__,
210        parallel_read_safe = True,
211        parallel_write_safe = True
212    )
213