diff options
Diffstat (limited to 'extract/src/docx_template_build.py')
-rwxr-xr-x | extract/src/docx_template_build.py | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/extract/src/docx_template_build.py b/extract/src/docx_template_build.py new file mode 100755 index 00000000..b528bcb5 --- /dev/null +++ b/extract/src/docx_template_build.py @@ -0,0 +1,210 @@ +#! /usr/bin/env python3 + +''' +Creates C code for creating docx files using internal template docx content. + +Args: + + -i <docx-path> + Set template docx file to extract from. + + -o <out-path> + Set name of output files. + + We write to <out-path>.c and <out-path>.h. +''' + +import io +import os +import re +import sys +import textwrap + + +def system(command): + ''' + Like os.system() but raises exception if command fails. + ''' + e = os.system(command) + if e: + print(f'command failed: {command}') + assert 0 + +def read(path): + ''' + Returns contents of file. We assume it is utf-8. + ''' + with open(path, 'rb') as f: + raw = f.read() + return raw.decode('utf-8') + +def write(text, path): + ''' + Writes text to file. + ''' + parent = os.path.dirname(path) + if parent: + os.makedirs(parent, exist_ok=True) + with open(path, 'w') as f: + f.write(text) + +def write_if_diff(text, path): + try: + old = read(path) + except Exception: + old = None + if text != old: + write(text, path) + +def check_path_safe(path): + ''' + Raises exception unless path consists only of characters and sequences that + are known to be safe for shell commands. + ''' + if '..' in path: + raise Exception(f'Path is unsafe because contains "..": {path!r}') + for c in path: + if not c.isalnum() and c not in '/._-': + #print(f'unsafe character {c} in: {path}') + raise Exception(f'Path is unsafe because contains "{c}": {path!r}') + +def path_safe(path): + ''' + Returns True if path is safe else False. + ''' + try: + check_path_safe(path) + except Exception: + return False + else: + return True + +assert not path_safe('foo;rm -rf *') +assert not path_safe('..') +assert path_safe('foo/bar.x') + + +def main(): + + path_in = None + path_out = None + args = iter(sys.argv[1:]) + while 1: + try: arg = next(args) + except StopIteration: break + if arg == '-h' or arg == '--help': + print(__doc__) + return + elif arg == '--docx-pretty': + d = next(args) + for dirpath, dirnames, filenames in os.walk(d): + for filename in filenames: + if not filename.endswith('.xml'): + continue + path = os.path.join(dirpath, filename) + system(f'xmllint --format {path} > {path}-') + system(f'mv {path}- {path}') + elif arg == '-i': + path_in = next(args) + elif arg == '-o': + path_out = next(args) + else: + assert 0 + + if not path_in: + return + + if not path_in: + raise Exception('Need to specify -i <docx-path>') + if not path_out: + raise Exception('Need to specify -o <out-path>') + + check_path_safe(path_in) + check_path_safe(path_out) + path_temp = f'{path_in}.dir' + os.system(f'rm -r "{path_temp}" 2>/dev/null') + system(f'unzip -q -d {path_temp} {path_in}') + + out_c = io.StringIO() + out_c.write(f'/* THIS IS AUTO-GENERATED CODE, DO NOT EDIT. */\n') + out_c.write(f'\n') + out_c.write(f'#include "{os.path.basename(path_out)}.h"\n') + out_c.write(f'\n') + + + out_c.write('const docx_template_item_t docx_template_items[] =\n') + out_c.write(f'{{\n') + + num_items = 0 + for dirpath, dirnames, filenames in os.walk(path_temp): + dirnames.sort() + + if 0: + # Write code to create directory item in zip. This isn't recognised by zipinfo, and doesn't + # make Word like the file. + # + name = dirpath[ len(path_temp)+1: ] + if name: + if not name.endswith('/'): + name += '/' + out_c3.write(f' if (extract_zip_write_file(zip, NULL, 0, "{name}")) goto end;\n') + + for filename in sorted(filenames): + num_items += 1 + path = os.path.join(dirpath, filename) + name = path[ len(path_temp)+1: ] + text = read(os.path.join(dirpath, filename)) + #print(f'first line is: %r' % text.split("\n")[0]) + text = text.replace('"', '\\"') + + # Looks like template files use \r\n when we interpret them as + # utf-8, so we preserve this in the generated strings. + # + text = text.replace('\r\n', '\\r\\n"\n "') + + # Split on '<' to avoid overly-long lines, which break windows + # compiler. + # + text = re.sub('([<][^/])', '"\n "\\1', text) + + # Remove name of document creator. + # + for tag in 'dc:creator', 'cp:lastModifiedBy': + text = re.sub(f'[<]{tag}[>][^<]*[<]/{tag}[>]', f'<{tag}></{tag}>', text) + + out_c.write(f' {{\n') + out_c.write(f' "{name}",\n') + out_c.write(f' "{text}"\n') + out_c.write(f' }},\n') + out_c.write(f' \n') + + out_c.write(f'}};\n') + out_c.write(f'\n') + out_c.write(f'int docx_template_items_num = {num_items};\n') + + out_c = out_c.getvalue() + write_if_diff(out_c, f'{path_out}.c') + + out_h = io.StringIO() + out_h.write(f'#ifndef EXTRACT_DOCX_TEMPLATE_H\n') + out_h.write(f'#define EXTRACT_DOCX_TEMPLATE_H\n') + out_h.write(f'\n') + out_h.write(f'/* THIS IS AUTO-GENERATED CODE, DO NOT EDIT. */\n') + out_h.write(f'\n') + out_h.write(f'\n') + out_h.write(f'typedef struct\n') + out_h.write(f'{{\n') + out_h.write(f' const char* name; /* Name of item in docx archive. */\n') + out_h.write(f' const char* text; /* Contents of item in docx archive. */\n') + out_h.write(f'}} docx_template_item_t;\n') + out_h.write(f'\n') + out_h.write(f'extern const docx_template_item_t docx_template_items[];\n') + out_h.write(f'extern int docx_template_items_num;\n') + out_h.write(f'\n') + out_h.write(f'\n') + out_h.write(f'#endif\n') + write_if_diff(out_h.getvalue(), f'{path_out}.h') + #os.system(f'rm -r "{path_temp}"') + +if __name__ == '__main__': + main() |