rst.py (6951B)
1 #!/usr/bin/env python 2 # encoding: utf-8 3 # Jérôme Carretero, 2013 (zougloub) 4 5 """ 6 reStructuredText support (experimental) 7 8 Example:: 9 10 def configure(conf): 11 conf.load('rst') 12 if not conf.env.RST2HTML: 13 conf.fatal('The program rst2html is required') 14 15 def build(bld): 16 bld( 17 features = 'rst', 18 type = 'rst2html', # rst2html, rst2pdf, ... 19 source = 'index.rst', # mandatory, the source 20 deps = 'image.png', # to give additional non-trivial dependencies 21 ) 22 23 By default the tool looks for a set of programs in PATH. 24 The tools are defined in `rst_progs`. 25 To configure with a special program use:: 26 27 $ RST2HTML=/path/to/rst2html waf configure 28 29 This tool is experimental; don't hesitate to contribute to it. 30 31 """ 32 33 import re 34 from waflib import Node, Utils, Task, Errors, Logs 35 from waflib.TaskGen import feature, before_method 36 37 rst_progs = "rst2html rst2xetex rst2latex rst2xml rst2pdf rst2s5 rst2man rst2odt rst2rtf".split() 38 39 def parse_rst_node(task, node, nodes, names, seen, dirs=None): 40 # TODO add extensibility, to handle custom rst include tags... 41 if dirs is None: 42 dirs = (node.parent,node.get_bld().parent) 43 44 if node in seen: 45 return 46 seen.append(node) 47 code = node.read() 48 re_rst = re.compile(r'^\s*.. ((?P<subst>\|\S+\|) )?(?P<type>include|image|figure):: (?P<file>.*)$', re.M) 49 for match in re_rst.finditer(code): 50 ipath = match.group('file') 51 itype = match.group('type') 52 Logs.debug('rst: visiting %s: %s', itype, ipath) 53 found = False 54 for d in dirs: 55 Logs.debug('rst: looking for %s in %s', ipath, d.abspath()) 56 found = d.find_node(ipath) 57 if found: 58 Logs.debug('rst: found %s as %s', ipath, found.abspath()) 59 nodes.append((itype, found)) 60 if itype == 'include': 61 parse_rst_node(task, found, nodes, names, seen) 62 break 63 if not found: 64 names.append((itype, ipath)) 65 66 class docutils(Task.Task): 67 """ 68 Compile a rst file. 69 """ 70 71 def scan(self): 72 """ 73 A recursive regex-based scanner that finds rst dependencies. 74 """ 75 76 nodes = [] 77 names = [] 78 seen = [] 79 80 node = self.inputs[0] 81 82 if not node: 83 return (nodes, names) 84 85 parse_rst_node(self, node, nodes, names, seen) 86 87 Logs.debug('rst: %r: found the following file deps: %r', self, nodes) 88 if names: 89 Logs.warn('rst: %r: could not find the following file deps: %r', self, names) 90 91 return ([v for (t,v) in nodes], [v for (t,v) in names]) 92 93 def check_status(self, msg, retcode): 94 """ 95 Check an exit status and raise an error with a particular message 96 97 :param msg: message to display if the code is non-zero 98 :type msg: string 99 :param retcode: condition 100 :type retcode: boolean 101 """ 102 if retcode != 0: 103 raise Errors.WafError('%r command exit status %r' % (msg, retcode)) 104 105 def run(self): 106 """ 107 Runs the rst compilation using docutils 108 """ 109 raise NotImplementedError() 110 111 class rst2html(docutils): 112 color = 'BLUE' 113 114 def __init__(self, *args, **kw): 115 docutils.__init__(self, *args, **kw) 116 self.command = self.generator.env.RST2HTML 117 self.attributes = ['stylesheet'] 118 119 def scan(self): 120 nodes, names = docutils.scan(self) 121 122 for attribute in self.attributes: 123 stylesheet = getattr(self.generator, attribute, None) 124 if stylesheet is not None: 125 ssnode = self.generator.to_nodes(stylesheet)[0] 126 nodes.append(ssnode) 127 Logs.debug('rst: adding dep to %s %s', attribute, stylesheet) 128 129 return nodes, names 130 131 def run(self): 132 cwdn = self.outputs[0].parent 133 src = self.inputs[0].path_from(cwdn) 134 dst = self.outputs[0].path_from(cwdn) 135 136 cmd = self.command + [src, dst] 137 cmd += Utils.to_list(getattr(self.generator, 'options', [])) 138 for attribute in self.attributes: 139 stylesheet = getattr(self.generator, attribute, None) 140 if stylesheet is not None: 141 stylesheet = self.generator.to_nodes(stylesheet)[0] 142 cmd += ['--%s' % attribute, stylesheet.path_from(cwdn)] 143 144 return self.exec_command(cmd, cwd=cwdn.abspath()) 145 146 class rst2s5(rst2html): 147 def __init__(self, *args, **kw): 148 rst2html.__init__(self, *args, **kw) 149 self.command = self.generator.env.RST2S5 150 self.attributes = ['stylesheet'] 151 152 class rst2latex(rst2html): 153 def __init__(self, *args, **kw): 154 rst2html.__init__(self, *args, **kw) 155 self.command = self.generator.env.RST2LATEX 156 self.attributes = ['stylesheet'] 157 158 class rst2xetex(rst2html): 159 def __init__(self, *args, **kw): 160 rst2html.__init__(self, *args, **kw) 161 self.command = self.generator.env.RST2XETEX 162 self.attributes = ['stylesheet'] 163 164 class rst2pdf(docutils): 165 color = 'BLUE' 166 def run(self): 167 cwdn = self.outputs[0].parent 168 src = self.inputs[0].path_from(cwdn) 169 dst = self.outputs[0].path_from(cwdn) 170 171 cmd = self.generator.env.RST2PDF + [src, '-o', dst] 172 cmd += Utils.to_list(getattr(self.generator, 'options', [])) 173 174 return self.exec_command(cmd, cwd=cwdn.abspath()) 175 176 177 @feature('rst') 178 @before_method('process_source') 179 def apply_rst(self): 180 """ 181 Create :py:class:`rst` or other rst-related task objects 182 """ 183 184 if self.target: 185 if isinstance(self.target, Node.Node): 186 tgt = self.target 187 elif isinstance(self.target, str): 188 tgt = self.path.get_bld().make_node(self.target) 189 else: 190 self.bld.fatal("rst: Don't know how to build target name %s which is not a string or Node for %s" % (self.target, self)) 191 else: 192 tgt = None 193 194 tsk_type = getattr(self, 'type', None) 195 196 src = self.to_nodes(self.source) 197 assert len(src) == 1 198 src = src[0] 199 200 if tsk_type is not None and tgt is None: 201 if tsk_type.startswith('rst2'): 202 ext = tsk_type[4:] 203 else: 204 self.bld.fatal("rst: Could not detect the output file extension for %s" % self) 205 tgt = src.change_ext('.%s' % ext) 206 elif tsk_type is None and tgt is not None: 207 out = tgt.name 208 ext = out[out.rfind('.')+1:] 209 self.type = 'rst2' + ext 210 elif tsk_type is not None and tgt is not None: 211 # the user knows what he wants 212 pass 213 else: 214 self.bld.fatal("rst: Need to indicate task type or target name for %s" % self) 215 216 deps_lst = [] 217 218 if getattr(self, 'deps', None): 219 deps = self.to_list(self.deps) 220 for filename in deps: 221 n = self.path.find_resource(filename) 222 if not n: 223 self.bld.fatal('Could not find %r for %r' % (filename, self)) 224 if not n in deps_lst: 225 deps_lst.append(n) 226 227 try: 228 task = self.create_task(self.type, src, tgt) 229 except KeyError: 230 self.bld.fatal("rst: Task of type %s not implemented (created by %s)" % (self.type, self)) 231 232 task.env = self.env 233 234 # add the manual dependencies 235 if deps_lst: 236 try: 237 lst = self.bld.node_deps[task.uid()] 238 for n in deps_lst: 239 if not n in lst: 240 lst.append(n) 241 except KeyError: 242 self.bld.node_deps[task.uid()] = deps_lst 243 244 inst_to = getattr(self, 'install_path', None) 245 if inst_to: 246 self.install_task = self.add_install_files(install_to=inst_to, install_from=task.outputs[:]) 247 248 self.source = [] 249 250 def configure(self): 251 """ 252 Try to find the rst programs. 253 254 Do not raise any error if they are not found. 255 You'll have to use additional code in configure() to die 256 if programs were not found. 257 """ 258 for p in rst_progs: 259 self.find_program(p, mandatory=False) 260