Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 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 | |
Peter Maydell | 80a046c | 2020-03-06 17:17:46 +0000 | [diff] [blame] | 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. |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 42 | class HxState(Enum): |
| 43 | CTEXT = 1 |
Peter Maydell | 80a046c | 2020-03-06 17:17:46 +0000 | [diff] [blame] | 44 | RST = 2 |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 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 | # |
Peter Maydell | 705f48c | 2020-02-28 15:36:08 +0000 | [diff] [blame] | 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) |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 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 | # |
Peter Maydell | 705f48c | 2020-02-28 15:36:08 +0000 | [diff] [blame] | 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) |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 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 |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 111 | elif directive == 'SRST': |
| 112 | if state == HxState.RST: |
| 113 | serror(hxfile, lnum, 'expected ERST, found SRST') |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 114 | else: |
| 115 | state = HxState.RST |
| 116 | elif directive == 'ERST': |
Peter Maydell | 80a046c | 2020-03-06 17:17:46 +0000 | [diff] [blame] | 117 | if state == HxState.CTEXT: |
Peter Maydell | 6803d6e | 2020-01-24 16:26:01 +0000 | [diff] [blame] | 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 | ) |