xref: /openbmc/qemu/docs/sphinx/hxtool.py (revision 988717b46b6424907618cb845ace9d69062703af)
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 
19 import os
20 import re
21 from enum import Enum
22 
23 from docutils import nodes
24 from docutils.statemachine import ViewList
25 from docutils.parsers.rst import directives, Directive
26 from sphinx.errors import ExtensionError
27 from sphinx.util.nodes import nested_parse_with_titles
28 import sphinx
29 
30 # Sphinx up to 1.6 uses AutodocReporter; 1.7 and later
31 # use switch_source_input. Check borrowed from kerneldoc.py.
32 Use_SSI = sphinx.__version__[:3] >= '1.7'
33 if Use_SSI:
34     from sphinx.util.docutils import switch_source_input
35 else:
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.
43 class HxState(Enum):
44     CTEXT = 1
45     TEXI = 2
46     RST = 3
47 
48 def 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 
52 def parse_directive(line):
53     """Return first word of line, if any"""
54     return re.split('\W', line)[0]
55 
56 def 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 
69 def 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 
81 class 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 
201 def 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