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.docutils import switch_source_input 28from sphinx.util.nodes import nested_parse_with_titles 29import sphinx 30 31 32__version__ = '1.0' 33 34# We parse hx files with a state machine which may be in one of two 35# states: reading the C code fragment, or inside a rST fragment. 36class HxState(Enum): 37 CTEXT = 1 38 RST = 2 39 40def serror(file, lnum, errtext): 41 """Raise an exception giving a user-friendly syntax error message""" 42 raise ExtensionError('%s line %d: syntax error: %s' % (file, lnum, errtext)) 43 44def parse_directive(line): 45 """Return first word of line, if any""" 46 return re.split(r'\W', line)[0] 47 48def parse_defheading(file, lnum, line): 49 """Handle a DEFHEADING directive""" 50 # The input should be "DEFHEADING(some string)", though note that 51 # the 'some string' could be the empty string. If the string is 52 # empty we ignore the directive -- these are used only to add 53 # blank lines in the plain-text content of the --help output. 54 # 55 # Return the heading text. We strip out any trailing ':' for 56 # consistency with other headings in the rST documentation. 57 match = re.match(r'DEFHEADING\((.*?):?\)', line) 58 if match is None: 59 serror(file, lnum, "Invalid DEFHEADING line") 60 return match.group(1) 61 62def parse_archheading(file, lnum, line): 63 """Handle an ARCHHEADING directive""" 64 # The input should be "ARCHHEADING(some string, other arg)", 65 # though note that the 'some string' could be the empty string. 66 # As with DEFHEADING, empty string ARCHHEADINGs will be ignored. 67 # 68 # Return the heading text. We strip out any trailing ':' for 69 # consistency with other headings in the rST documentation. 70 match = re.match(r'ARCHHEADING\((.*?):?,.*\)', line) 71 if match is None: 72 serror(file, lnum, "Invalid ARCHHEADING line") 73 return match.group(1) 74 75def parse_srst(file, lnum, line): 76 """Handle an SRST directive""" 77 # The input should be either "SRST", or "SRST(label)". 78 match = re.match(r'SRST(\((.*?)\))?', line) 79 if match is None: 80 serror(file, lnum, "Invalid SRST line") 81 return match.group(2) 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 == 'SRST': 114 if state == HxState.RST: 115 serror(hxfile, lnum, 'expected ERST, found SRST') 116 else: 117 state = HxState.RST 118 label = parse_srst(hxfile, lnum, line) 119 if label: 120 rstlist.append("", hxfile, lnum - 1) 121 # Build label as _DOCNAME-HXNAME-LABEL 122 hx = os.path.splitext(os.path.basename(hxfile))[0] 123 refline = ".. _" + env.docname + "-" + hx + \ 124 "-" + label + ":" 125 rstlist.append(refline, hxfile, lnum - 1) 126 elif directive == 'ERST': 127 if state == HxState.CTEXT: 128 serror(hxfile, lnum, 'expected SRST, found ERST') 129 else: 130 state = HxState.CTEXT 131 elif directive == 'DEFHEADING' or directive == 'ARCHHEADING': 132 if directive == 'DEFHEADING': 133 heading = parse_defheading(hxfile, lnum, line) 134 else: 135 heading = parse_archheading(hxfile, lnum, line) 136 if heading == "": 137 continue 138 # Put the accumulated rST into the previous node, 139 # and then start a fresh section with this heading. 140 if len(rstlist) > 0: 141 if current_node is None: 142 # We had some rST fragments before the first 143 # DEFHEADING. We don't have a section to put 144 # these in, so rather than magicing up a section, 145 # make it a syntax error. 146 serror(hxfile, lnum, 147 'first DEFHEADING must precede all rST text') 148 self.do_parse(rstlist, current_node) 149 rstlist = ViewList() 150 if current_node is not None: 151 node_list.append(current_node) 152 section_id = 'hxtool-%d' % env.new_serialno('hxtool') 153 current_node = nodes.section(ids=[section_id]) 154 current_node += nodes.title(heading, heading) 155 else: 156 # Not a directive: put in output if we are in rST fragment 157 if state == HxState.RST: 158 # Sphinx counts its lines from 0 159 rstlist.append(line, hxfile, lnum - 1) 160 161 if current_node is None: 162 # We don't have multiple sections, so just parse the rst 163 # fragments into a dummy node so we can return the children. 164 current_node = nodes.section() 165 self.do_parse(rstlist, current_node) 166 return current_node.children 167 else: 168 # Put the remaining accumulated rST into the last section, and 169 # return all the sections. 170 if len(rstlist) > 0: 171 self.do_parse(rstlist, current_node) 172 node_list.append(current_node) 173 return node_list 174 175 # This is from kerneldoc.py -- it works around an API change in 176 # Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use 177 # sphinx.util.nodes.nested_parse_with_titles() rather than the 178 # plain self.state.nested_parse(), and so we can drop the saving 179 # of title_styles and section_level that kerneldoc.py does, 180 # because nested_parse_with_titles() does that for us. 181 def do_parse(self, result, node): 182 with switch_source_input(self.state, result): 183 nested_parse_with_titles(self.state, result, node) 184 185 186def setup(app): 187 """ Register hxtool-doc directive with Sphinx""" 188 app.add_config_value('hxtool_srctree', None, 'env') 189 app.add_directive('hxtool-doc', HxtoolDocDirective) 190 191 return dict( 192 version = __version__, 193 parallel_read_safe = True, 194 parallel_write_safe = True 195 ) 196