1#!/usr/local/bin/python3.8
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