xref: /openbmc/qemu/docs/sphinx/hxtool.py (revision 91aef87a)
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
64    match = re.match(r'DEFHEADING\((.*)\)', line)
65    if match is None:
66        serror(file, lnum, "Invalid DEFHEADING line")
67    return match.group(1)
68
69def parse_archheading(file, lnum, line):
70    """Handle an ARCHHEADING directive"""
71    # The input should be "ARCHHEADING(some string, other arg)",
72    # though note that the 'some string' could be the empty string.
73    # As with DEFHEADING, empty string ARCHHEADINGs will be ignored.
74    #
75    # Return the heading text
76    match = re.match(r'ARCHHEADING\((.*),.*\)', line)
77    if match is None:
78        serror(file, lnum, "Invalid ARCHHEADING line")
79    return match.group(1)
80
81class HxtoolDocDirective(Directive):
82    """Extract rST fragments from the specified .hx file"""
83    required_argument = 1
84    optional_arguments = 1
85    option_spec = {
86        'hxfile': directives.unchanged_required
87    }
88    has_content = False
89
90    def run(self):
91        env = self.state.document.settings.env
92        hxfile = env.config.hxtool_srctree + '/' + self.arguments[0]
93
94        # Tell sphinx of the dependency
95        env.note_dependency(os.path.abspath(hxfile))
96
97        state = HxState.CTEXT
98        # We build up lines of rST in this ViewList, which we will
99        # later put into a 'section' node.
100        rstlist = ViewList()
101        current_node = None
102        node_list = []
103
104        with open(hxfile) as f:
105            lines = (l.rstrip() for l in f)
106            for lnum, line in enumerate(lines, 1):
107                directive = parse_directive(line)
108
109                if directive == 'HXCOMM':
110                    pass
111                elif directive == 'STEXI':
112                    if state == HxState.RST:
113                        serror(hxfile, lnum, 'expected ERST, found STEXI')
114                    elif state == HxState.TEXI:
115                        serror(hxfile, lnum, 'expected ETEXI, found STEXI')
116                    else:
117                        state = HxState.TEXI
118                elif directive == 'ETEXI':
119                    if state == HxState.RST:
120                        serror(hxfile, lnum, 'expected ERST, found ETEXI')
121                    elif state == HxState.CTEXT:
122                        serror(hxfile, lnum, 'expected STEXI, found ETEXI')
123                    else:
124                        state = HxState.CTEXT
125                elif directive == 'SRST':
126                    if state == HxState.RST:
127                        serror(hxfile, lnum, 'expected ERST, found SRST')
128                    elif state == HxState.TEXI:
129                        serror(hxfile, lnum, 'expected ETEXI, found SRST')
130                    else:
131                        state = HxState.RST
132                elif directive == 'ERST':
133                    if state == HxState.TEXI:
134                        serror(hxfile, lnum, 'expected ETEXI, found ERST')
135                    elif state == HxState.CTEXT:
136                        serror(hxfile, lnum, 'expected SRST, found ERST')
137                    else:
138                        state = HxState.CTEXT
139                elif directive == 'DEFHEADING' or directive == 'ARCHHEADING':
140                    if directive == 'DEFHEADING':
141                        heading = parse_defheading(hxfile, lnum, line)
142                    else:
143                        heading = parse_archheading(hxfile, lnum, line)
144                    if heading == "":
145                        continue
146                    # Put the accumulated rST into the previous node,
147                    # and then start a fresh section with this heading.
148                    if len(rstlist) > 0:
149                        if current_node is None:
150                            # We had some rST fragments before the first
151                            # DEFHEADING. We don't have a section to put
152                            # these in, so rather than magicing up a section,
153                            # make it a syntax error.
154                            serror(hxfile, lnum,
155                                   'first DEFHEADING must precede all rST text')
156                        self.do_parse(rstlist, current_node)
157                        rstlist = ViewList()
158                    if current_node is not None:
159                        node_list.append(current_node)
160                    section_id = 'hxtool-%d' % env.new_serialno('hxtool')
161                    current_node = nodes.section(ids=[section_id])
162                    current_node += nodes.title(heading, heading)
163                else:
164                    # Not a directive: put in output if we are in rST fragment
165                    if state == HxState.RST:
166                        # Sphinx counts its lines from 0
167                        rstlist.append(line, hxfile, lnum - 1)
168
169        if current_node is None:
170            # We don't have multiple sections, so just parse the rst
171            # fragments into a dummy node so we can return the children.
172            current_node = nodes.section()
173            self.do_parse(rstlist, current_node)
174            return current_node.children
175        else:
176            # Put the remaining accumulated rST into the last section, and
177            # return all the sections.
178            if len(rstlist) > 0:
179                self.do_parse(rstlist, current_node)
180            node_list.append(current_node)
181            return node_list
182
183    # This is from kerneldoc.py -- it works around an API change in
184    # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use
185    # sphinx.util.nodes.nested_parse_with_titles() rather than the
186    # plain self.state.nested_parse(), and so we can drop the saving
187    # of title_styles and section_level that kerneldoc.py does,
188    # because nested_parse_with_titles() does that for us.
189    def do_parse(self, result, node):
190        if Use_SSI:
191            with switch_source_input(self.state, result):
192                nested_parse_with_titles(self.state, result, node)
193        else:
194            save = self.state.memo.reporter
195            self.state.memo.reporter = AutodocReporter(result, self.state.memo.reporter)
196            try:
197                nested_parse_with_titles(self.state, result, node)
198            finally:
199                self.state.memo.reporter = save
200
201def setup(app):
202    """ Register hxtool-doc directive with Sphinx"""
203    app.add_config_value('hxtool_srctree', None, 'env')
204    app.add_directive('hxtool-doc', HxtoolDocDirective)
205
206    return dict(
207        version = __version__,
208        parallel_read_safe = True,
209        parallel_write_safe = True
210    )
211