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 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 69def 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 81class 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 201def 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