]>
Commit | Line | Data |
---|---|---|
6803d6e9 PM |
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 | ||
80a046c5 PM |
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. | |
6803d6e9 PM |
42 | class HxState(Enum): |
43 | CTEXT = 1 | |
80a046c5 | 44 | RST = 2 |
6803d6e9 PM |
45 | |
46 | def 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 | ||
50 | def parse_directive(line): | |
51 | """Return first word of line, if any""" | |
52 | return re.split('\W', line)[0] | |
53 | ||
54 | def 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 | # | |
705f48cc PM |
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) | |
6803d6e9 PM |
64 | if match is None: |
65 | serror(file, lnum, "Invalid DEFHEADING line") | |
66 | return match.group(1) | |
67 | ||
68 | def 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 | # | |
705f48cc PM |
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) | |
6803d6e9 PM |
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 | |
6803d6e9 PM |
111 | elif directive == 'SRST': |
112 | if state == HxState.RST: | |
113 | serror(hxfile, lnum, 'expected ERST, found SRST') | |
6803d6e9 PM |
114 | else: |
115 | state = HxState.RST | |
116 | elif directive == 'ERST': | |
80a046c5 | 117 | if state == HxState.CTEXT: |
6803d6e9 PM |
118 | serror(hxfile, lnum, 'expected SRST, found ERST') |
119 | else: | |
120 | state = HxState.CTEXT | |
121 | elif directive == 'DEFHEADING' or directive == 'ARCHHEADING': | |
122 | if directive == 'DEFHEADING': | |
123 | heading = parse_defheading(hxfile, lnum, line) | |
124 | else: | |
125 | heading = parse_archheading(hxfile, lnum, line) | |
126 | if heading == "": | |
127 | continue | |
128 | # Put the accumulated rST into the previous node, | |
129 | # and then start a fresh section with this heading. | |
130 | if len(rstlist) > 0: | |
131 | if current_node is None: | |
132 | # We had some rST fragments before the first | |
133 | # DEFHEADING. We don't have a section to put | |
134 | # these in, so rather than magicing up a section, | |
135 | # make it a syntax error. | |
136 | serror(hxfile, lnum, | |
137 | 'first DEFHEADING must precede all rST text') | |
138 | self.do_parse(rstlist, current_node) | |
139 | rstlist = ViewList() | |
140 | if current_node is not None: | |
141 | node_list.append(current_node) | |
142 | section_id = 'hxtool-%d' % env.new_serialno('hxtool') | |
143 | current_node = nodes.section(ids=[section_id]) | |
144 | current_node += nodes.title(heading, heading) | |
145 | else: | |
146 | # Not a directive: put in output if we are in rST fragment | |
147 | if state == HxState.RST: | |
148 | # Sphinx counts its lines from 0 | |
149 | rstlist.append(line, hxfile, lnum - 1) | |
150 | ||
151 | if current_node is None: | |
152 | # We don't have multiple sections, so just parse the rst | |
153 | # fragments into a dummy node so we can return the children. | |
154 | current_node = nodes.section() | |
155 | self.do_parse(rstlist, current_node) | |
156 | return current_node.children | |
157 | else: | |
158 | # Put the remaining accumulated rST into the last section, and | |
159 | # return all the sections. | |
160 | if len(rstlist) > 0: | |
161 | self.do_parse(rstlist, current_node) | |
162 | node_list.append(current_node) | |
163 | return node_list | |
164 | ||
165 | # This is from kerneldoc.py -- it works around an API change in | |
166 | # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use | |
167 | # sphinx.util.nodes.nested_parse_with_titles() rather than the | |
168 | # plain self.state.nested_parse(), and so we can drop the saving | |
169 | # of title_styles and section_level that kerneldoc.py does, | |
170 | # because nested_parse_with_titles() does that for us. | |
171 | def do_parse(self, result, node): | |
172 | if Use_SSI: | |
173 | with switch_source_input(self.state, result): | |
174 | nested_parse_with_titles(self.state, result, node) | |
175 | else: | |
176 | save = self.state.memo.reporter | |
177 | self.state.memo.reporter = AutodocReporter(result, self.state.memo.reporter) | |
178 | try: | |
179 | nested_parse_with_titles(self.state, result, node) | |
180 | finally: | |
181 | self.state.memo.reporter = save | |
182 | ||
183 | def setup(app): | |
184 | """ Register hxtool-doc directive with Sphinx""" | |
185 | app.add_config_value('hxtool_srctree', None, 'env') | |
186 | app.add_directive('hxtool-doc', HxtoolDocDirective) | |
187 | ||
188 | return dict( | |
189 | version = __version__, | |
190 | parallel_read_safe = True, | |
191 | parallel_write_safe = True | |
192 | ) |