1#!/usr/bin/python 2 3# Copyright Abel Sinkovics (abel@sinkovics.hu) 2015. 4# Distributed under the Boost Software License, Version 1.0. 5# (See accompanying file LICENSE_1_0.txt or copy at 6# http://www.boost.org/LICENSE_1_0.txt) 7 8import sys 9import argparse 10import re 11import os 12 13def remove_last_dot(s): 14 if s.endswith('.'): 15 return s[:-1] 16 else: 17 return s 18 19def remove_newline(s): 20 return re.sub('[\r\n]', '', s) 21 22def is_definition(s): 23 cmd = s.strip() 24 25 def_prefixes = ['#include ', 'using ', 'struct ', 'template '] 26 return any([cmd.startswith(s) for s in def_prefixes]) or cmd.endswith(';') 27 28def prefix_lines(prefix, s): 29 return '\n'.join(['%s%s' % (prefix, l) for l in s.split('\n')]) 30 31def protect_metashell(s): 32 if s.startswith('#include <metashell'): 33 return '#ifdef __METASHELL\n%s\n#endif' % (s) 34 else: 35 return s 36 37def parse_md(qbk): 38 sections = [] 39 defs = [] 40 current_section = '' 41 in_cpp_snippet = False 42 numbered_section_header = re.compile('^\[section *([0-9.]+)') 43 metashell_command = re.compile('^> [^ ]') 44 metashell_prompt = re.compile('^(\.\.\.|)>') 45 msh_cmd = '' 46 for l in qbk: 47 if l.startswith(' '): 48 ll = l[2:] 49 if not in_cpp_snippet: 50 in_msh_cpp_snippet = True 51 if in_msh_cpp_snippet: 52 if metashell_command.match(ll) or msh_cmd != '': 53 cmd = metashell_prompt.sub('', remove_newline(ll)) 54 if msh_cmd != '': 55 msh_cmd = msh_cmd + '\n' 56 msh_cmd = msh_cmd + cmd 57 if msh_cmd.endswith('\\'): 58 msh_cmd = msh_cmd[:-1].strip() + ' ' 59 else: 60 if not is_definition(msh_cmd): 61 msh_cmd = '// query:\n%s' % (prefix_lines('// ', msh_cmd)) 62 defs.append((current_section, protect_metashell(msh_cmd.strip()))) 63 msh_cmd = '' 64 elif not in_cpp_snippet: 65 in_msh_cpp_snippet = False 66 in_cpp_snippet = True 67 else: 68 in_cpp_snippet = False 69 m = numbered_section_header.match(l) 70 if m: 71 current_section = remove_last_dot(m.group(1)).replace('.', '_') 72 sections.append(current_section) 73 74 sections.sort(key = lambda s: [int(n) for n in s.split('_')]) 75 return (sections, defs) 76 77def delete_old_headers(path): 78 for f in os.listdir(path): 79 if f.endswith('.hpp'): 80 os.remove(os.path.join(path, f)) 81 82def gen_headers(sections, defs, path): 83 files = {} 84 85 prev_section = '' 86 for s in sections: 87 prev_name = prev_section.replace('_', '.') 88 include_guard = 'BOOST_METAPARSE_GETTING_STARTED_%s_HPP' % (s) 89 if prev_section == '': 90 prev_include = '' 91 else: 92 prev_include = \ 93 '// Definitions before section {0}\n'.format(prev_name) + \ 94 '#include "{0}.hpp"\n'.format(prev_section) + \ 95 '\n' 96 97 files[os.path.join(path, s + '.hpp')] = \ 98 '#ifndef {0}\n'.format(include_guard) + \ 99 '#define {0}\n'.format(include_guard) + \ 100 '\n' + \ 101 '// Automatically generated header file\n' + \ 102 '\n' + \ 103 prev_include + \ 104 '// Definitions of section {0}\n'.format(prev_name) + \ 105 '\n'.join( \ 106 ['%s\n' % (d) for (sec, d) in defs if sec == prev_section] \ 107 ) + \ 108 '\n' + \ 109 '#endif\n' + \ 110 '\n' 111 prev_section = s 112 return files 113 114def remove_metashell_protection(s): 115 prefix = '#ifdef __METASHELL\n' 116 suffix = '#endif' 117 return \ 118 s[len(prefix):-len(suffix)] \ 119 if s.startswith(prefix) and s.endswith(suffix) \ 120 else s 121 122def make_code_snippet(s): 123 return '\n'.join([' {0}'.format(l) for l in s.split('\n')]) 124 125def what_we_have_so_far_docs(doc_dir, qbk, defs, sections): 126 files = {} 127 so_far = '' 128 sections_with_definition = [] 129 for s in sections: 130 if so_far != '': 131 files[os.path.join(doc_dir, 'before_{0}.qbk'.format(s))] = \ 132 '[#before_{0}]\n[\'Definitions before section {1}]\n\n{2}\n'.format( 133 s, 134 s.replace('_', '.') + '.', 135 so_far 136 ) 137 sections_with_definition.append(s) 138 139 so_far = so_far + '\n'.join([ 140 '{0}\n'.format(make_code_snippet(remove_metashell_protection(d))) 141 for (sec, d) in defs 142 if sec == s and not d.startswith('//') 143 ]) 144 145 is_section = re.compile('^\[section (([0-9]\.)+)') 146 note_prefix = \ 147 '[note Note that you can find everything that has been included and' \ 148 ' defined so far [link before_' 149 150 in_definitions_before_each_section = False 151 152 result = [] 153 for l in qbk: 154 if in_definitions_before_each_section: 155 if l.strip() == '[endsect]': 156 in_definitions_before_each_section = False 157 result.append(l) 158 elif l.strip() == '[section Definitions before each section]': 159 in_definitions_before_each_section = True 160 result.append(l) 161 result.append('\n') 162 for s in sections_with_definition: 163 result.append('[include before_{0}.qbk]\n'.format(s)) 164 result.append('\n') 165 elif not l.startswith(note_prefix): 166 result.append(l) 167 m = is_section.match(l) 168 if m: 169 section_number = m.group(1).replace('.', '_')[:-1] 170 if section_number in sections_with_definition: 171 result.append('{0}{1} here].]\n'.format(note_prefix, section_number)) 172 173 return (files, result) 174 175def strip_not_finished_line(s): 176 s = s.strip() 177 return s[:-1] if s.endswith('\\') else s 178 179def make_copy_paste_friendly(lines): 180 result = [] 181 for l in lines: 182 if l.startswith('> '): 183 result.append(l[2:]) 184 elif l.startswith('...> '): 185 result[-1] = strip_not_finished_line(result[-1]) + l[5:].lstrip() 186 return result 187 188def extract_code_snippets(qbk, fn_base): 189 code_prefix = ' ' 190 191 files = {} 192 193 result = [] 194 in_cpp_code = False 195 counter = 0 196 in_copy_paste_friendly_examples = False 197 skip_empty_lines = False 198 for l in qbk: 199 if l.strip() != '' or not skip_empty_lines: 200 skip_empty_lines = False 201 if in_copy_paste_friendly_examples: 202 if 'endsect' in l: 203 in_copy_paste_friendly_examples = False 204 result.append('\n') 205 result.extend([ 206 '[include {0}_{1}.qbk]\n'.format(re.sub('^.*/', '', fn_base), i) \ 207 for i in range(0, counter) 208 ]) 209 result.append('\n') 210 result.append(l) 211 in_copy_paste_friendly_examples = False 212 elif '[section Copy-paste friendly code examples]' in l: 213 in_copy_paste_friendly_examples = True 214 result.append(l) 215 elif 'copy-paste friendly version' in l: 216 skip_empty_lines = True 217 else: 218 result.append(l) 219 220 if in_cpp_code: 221 if not l.startswith(code_prefix): 222 in_cpp_code = False 223 if len(code) > 1: 224 f = '{0}_{1}'.format(fn_base, counter) 225 basename_f = re.sub('^.*/', '', f) 226 files['{0}.qbk'.format(f)] = \ 227 '[#{0}]\n\n{1}\n'.format( 228 basename_f, 229 ''.join( 230 [code_prefix + s for s in make_copy_paste_friendly(code)] 231 ) 232 ) 233 result.append( 234 '[link {0} copy-paste friendly version]\n'.format(basename_f) 235 ) 236 result.append('\n') 237 counter = counter + 1 238 elif \ 239 l.startswith(code_prefix + '> ') \ 240 or l.startswith(code_prefix + '...> '): 241 code.append(l[len(code_prefix):]) 242 elif l.startswith(code_prefix): 243 in_cpp_code = True 244 code = [l[len(code_prefix):]] 245 246 return (files, result) 247 248def write_file(fn, content): 249 with open(fn, 'w') as f: 250 f.write(content) 251 252def write_files(files): 253 for fn in files: 254 write_file(fn, files[fn]) 255 256def main(): 257 desc = 'Generate headers with the definitions of a Getting Started guide' 258 parser = argparse.ArgumentParser(description=desc) 259 parser.add_argument( 260 '--src', 261 dest='src', 262 default='doc/getting_started.qbk', 263 help='The .qbk source of the Getting Started guide' 264 ) 265 parser.add_argument( 266 '--dst', 267 dest='dst', 268 default='example/getting_started', 269 help='The target directory to generate into (all headers in that directory will be deleted!)' 270 ) 271 272 args = parser.parse_args() 273 274 qbk = open(args.src, 'r').readlines() 275 276 delete_old_headers(args.dst) 277 doc_dir = os.path.dirname(args.src) 278 279 (sections, defs) = parse_md(qbk) 280 files1 = gen_headers(sections, defs, args.dst) 281 (files2, qbk) = what_we_have_so_far_docs(doc_dir, qbk, defs, sections) 282 (files3, qbk) = \ 283 extract_code_snippets( 284 qbk, 285 args.src[:-4] if args.src.endswith('.qbk') else args.src 286 ) 287 288 write_files(files1) 289 write_files(files2) 290 write_files(files3) 291 write_file(args.src, ''.join(qbk)) 292 293if __name__ == "__main__": 294 main() 295 296