3 # A script that splits a Markdown file into plain text (for spell checking) and c++ files.
6 from __future__
import absolute_import
, print_function
, unicode_literals
14 TAG_REGEX
= re
.compile(r
'(<!--.*?-->|<[^>]*>)')
15 NAMED_A_TAG_REGEX
= re
.compile(r
'.*name ?= ?"([^"]*)"')
19 This script ended up ugly, so in case somebody wants to reimplement, here is the spec that grew by time.
21 What it should do it take a markdown file, and split it into more files. A targetfile should have the same
22 number of lines as the original, with source code snippets and markdown non-words removed, for spell-checking.
24 Each code snipped should go into a separate file in codedir.
26 Each code snipped should get additional C++ code around it to help compile the line in context, with
27 some heuristic guessing of what is needed around. The wrapping code should have a token in each line allowing
28 other tools to filter out these lines
30 The name for each file chosen consists os the section id in the markdown document, a counter for the snippet inside the section.
32 Snippets without code (only comments) or containing lines starting with ??? should not yeld files,
33 but the counter for naming snippets should still increment.
35 parser
= argparse
.ArgumentParser(description
='Split md file into plain text and code blocks')
36 parser
.add_argument('sourcefile',
37 help='which file to read')
38 parser
.add_argument('targetfile',
39 help='where to put plain text')
40 parser
.add_argument('codedir',
41 help='where to put codeblocks')
42 args
= parser
.parse_args()
44 # ensure folder exists
45 if not os
.path
.exists(args
.codedir
):
46 os
.makedirs(args
.codedir
)
49 if os
.path
.exists(args
.targetfile
):
50 os
.remove(args
.targetfile
)
55 with io
.open(args
.sourcefile
, 'r') as read_filehandle
:
56 with io
.open(args
.targetfile
, 'w') as text_filehandle
:
57 for line
in read_filehandle
:
59 indent_depth
= is_code(line
)
61 (line
, linenum
) = process_code(read_filehandle
,
64 args
.sourcefile
, args
.codedir
,
65 last_header
, code_block_index
,
68 # reach here either line was not code, or was code
69 # and we dealt with n code lines
70 if indent_depth
< 4 or not is_code(line
, indent_depth
):
71 # store header id for codeblock
72 section_id
= get_marker(line
)
73 if section_id
is not None:
75 last_header
= section_id
76 sline
= stripped(line
)
77 text_filehandle
.write(sline
)
79 assert line_length(args
.sourcefile
) == line_length(args
.targetfile
)
82 def process_code(read_filehandle
, text_filehandle
, line
, linenum
, sourcefile
, codedir
, name
, index
, indent_depth
):
83 fenced
= (line
.strip() == '```')
86 line
= read_filehandle
.readLine()
88 text_filehandle
.write('\n')
91 start_linenum
= linenum
92 has_actual_code
= False
93 has_question_marks
= False
95 while ((fenced
and line
.strip() != '```') or (not fenced
and is_inside_code(line
, indent_depth
))):
96 # copy comments to plain text for spell check
97 comment_idx
= line
.find('//')
98 no_comment_line
= line
100 no_comment_line
= line
[:comment_idx
].strip()
101 text_filehandle
.write(line
[comment_idx
+ 2:])
103 # write empty line so line numbers stay stable
104 text_filehandle
.write('\n')
106 if (not has_actual_code
107 and not line
.strip().startswith('//')
108 and not line
.strip().startswith('???')
109 and not line
.strip() == ''):
110 has_actual_code
= True
112 if (not line
.strip() == '```'):
113 if ('???' == no_comment_line
or '...' == no_comment_line
):
114 has_question_marks
= True
115 linebuffer
.append(dedent(line
, indent_depth
) if not fenced
else line
)
117 line
= read_filehandle
.readline()
119 except StopIteration:
122 codefile
= os
.path
.join(codedir
, '%s%s.cpp' % (name
, index
))
124 text_filehandle
.write('\n')
126 if (has_actual_code
and not has_question_marks
):
127 linebuffer
= clean_trailing_newlines(linebuffer
)
128 write_with_harness(codefile
, sourcefile
, start_linenum
, linebuffer
)
129 return (line
, linenum
)
132 def clean_trailing_newlines(linebuffer
):
136 for line
in linebuffer
:
137 if not code_started
and line
== '\n':
145 def write_with_harness(codefile
, sourcefile
, start_linenum
, linebuffer
):
146 '''write output with additional lines to make code likely compilable'''
147 # add commonly used headers, so that lines can likely compile.
148 # This is work in progress, the main issue remains handling class
149 # declarations in in-function code differently
150 with io
.open(codefile
, 'w') as code_filehandle
:
151 code_filehandle
.write('''\
152 #include<stdio.h> // by md-split
153 #include<stdlib.h> // by md-split
154 #include<tuple> // by md-split
155 #include<utility> // by md-split
156 #include<limits> // by md-split
157 #include<functional> // by md-split
158 #include<string> // by md-split
159 #include<map> // by md-split
160 #include<iostream> // by md-split
161 #include<vector> // by md-split
162 #include<algorithm> // by md-split
163 #include<memory> // by md-split
164 using namespace std; // by md-split
166 ''' % (sourcefile
, start_linenum
))
167 # TODO: if not toplevel code, wrap inside class
168 for codeline
in linebuffer
:
169 code_filehandle
.write(codeline
)
172 def is_code(line
, indent_depth
= 4):
173 '''returns the indent depth, 0 means not code in markup'''
174 if line
.startswith(' ' * indent_depth
):
175 return len(line
) - len(line
.lstrip(' '))
178 def is_inside_code(line
, indent_depth
):
179 return is_code(line
, indent_depth
) > 0 or line
.strip() == ''
182 # Remove well-formed html tags, fixing mistakes by legitimate users
183 sline
= TAG_REGEX
.sub('', line
)
184 sline
= re
.sub('[()\[\]#*]', ' ', line
)
187 def dedent(line
, indent_depth
):
188 if line
.startswith(' ' * indent_depth
):
189 return line
[indent_depth
:]
190 if line
.startswith('\t'):
194 def get_marker(line
):
195 matchlist
= TAG_REGEX
.findall(line
)
197 namematch
= NAMED_A_TAG_REGEX
.match(line
)
199 return namematch
.group(1) # group 0 is full match
203 def line_length(filename
):
204 return sum(1 for line
in open(filename
))
206 if __name__
== '__main__':