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