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