#!/usr/bin/python3 # Copyright 2017 QvarnLabs Ab # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # =*= License: GPL-3+ =*= import os import shutil import subprocess import sys import tempfile import cliapp def write_file(content, filename): with open(filename, 'w') as f: f.write(content) return filename def get_uml_title(lines): prefix = 'title ' for line in lines: line = line.strip() if line.startswith(prefix): return line[len(prefix):] return 'title' def format_uml(filename): subprocess.check_call(['plantuml', '-tpng', '-output', '.', filename]) def remove_prefixes(lines): prefix = ' ' * 4 result = [] for line in lines: if line.startswith(prefix): line = line[len(prefix):] result.append(line) return ''.join(result) def process_uml(lines, tgtdir, relative, mtime): title = get_uml_title(lines) uml_text = remove_prefixes(lines) uml_filename = os.path.join(tgtdir, relative + '.uml') png_basename = os.path.join(relative + '.png') png_filename = os.path.join(tgtdir, png_basename) write_file(uml_text, uml_filename) format_uml(uml_filename) include = '[[!img {}]]\n'.format(png_basename) return [include] def format_roadmap(text): input = text.encode('utf-8') output = cliapp.runcmd(['projgraph'], feed_stdin=input) output_text = output.decode('utf-8') output_lines = output_text.splitlines() return '\n'.join(output_lines[1:-1]) def process_roadmap(lines, tgtdir, relative, mtime): roadmap_text = remove_prefixes(lines[1:-1]) include = format_roadmap(roadmap_text) return ['[[!graph src="""\n', include, '"""]]\n'] def read_lines(filename): with open(filename) as f: return f.readlines() def write_lines(filename, lines): with open(filename, 'w') as f: f.write(''.join(lines)) def line_matches(line, patterns): prefix = ' ' * 4 for i, pattern in enumerate(patterns): if line in [pattern, prefix + pattern]: return i return None def find_match(lines, patterns): for i, line in enumerate(lines): j = line_matches(line, patterns) if j is not None: return i, j else: return None, None def find_section(lines, sections): starters = [x for x, _, _ in sections] enders = [x for _, x, _ in sections] funcs = [x for _, _, x in sections] first, section = find_match(lines, starters) if first is not None: last, _ = find_match(lines, enders[section:section+1]) if last is not None: return first, last+1, funcs[section] return 0, len(lines), None def preprocess_lines(lines, tgtdir, relative, mtime): sections = [ ('@startuml\n', '@enduml\n', process_uml), ('@startroadmap\n', '@endroadmap\n', process_roadmap), ] result = [] counter = 0 while lines: start, after, func = find_section(lines, sections) result.extend(lines[:start]) section = lines[start:after] if func: counter += 1 output = '{}-{}'.format(relative, counter) include = func(section, tgtdir, output, mtime) result.extend(include) else: result.extend(section) lines = lines[after:] return result def needs_preprocessing(src, tgt): if not os.path.exists(tgt): return True src_mtime = os.path.getmtime(src) tgt_mtime = os.path.getmtime(tgt) return src_mtime != tgt_mtime def preprocess_mdwn(srcdir, tgtdir, relative): src = make_absolute(srcdir, relative) tgt = make_absolute(tgtdir, relative) if needs_preprocessing(src, tgt): src_mtime = os.path.getmtime(src) lines = read_lines(src) lines = preprocess_lines(lines, tgtdir, relative, src_mtime) write_lines(tgt, lines) shutil.copystat(src, tgt) def copy_file(srcdir, tgtdir, relative): src = make_absolute(srcdir, relative) tgt = make_absolute(tgtdir, relative) shutil.copy(src, tgt) shutil.copystat(src, tgt) def preprocess_file(srcdir, tgtdir, relative): if relative.endswith('.mdwn'): preprocess_mdwn(srcdir, tgtdir, relative) else: copy_file(srcdir, tgtdir, relative) def copy_dir(srcdir, tgtdir, relative): src = make_absolute(srcdir, relative) tgt = make_absolute(tgtdir, relative) if not os.path.exists(tgt): os.makedirs(tgt) return lambda: shutil.copystat(src, tgt) def acceptable(basename): return basename != '.git' def make_relative(dirname, pathname): prefix = dirname if not prefix.endswith('/'): prefix += '/' assert pathname == dirname or pathname.startswith(prefix) if pathname == dirname: return '.' else: return pathname[len(prefix):] def find_all(root): for dirpath, subdirs, filenames in os.walk(root): yield dirpath for x in subdirs[:]: if not acceptable(x): subdirs.remove(x) for basename in filenames: yield os.path.join(dirpath, basename) def find_relative(dirname): for pathname in find_all(dirname): yield make_relative(dirname, pathname) def make_absolute(root, relative): return os.path.join(root, relative) def preprocess(srcdir, tgtdir): preprocessors = [ (os.path.isfile, preprocess_file), (os.path.isdir, copy_dir), ] assert os.path.isdir(srcdir) # assert not os.path.exists(tgtdir) todo = [] for pathname in find_relative(srcdir): src = make_absolute(srcdir, pathname) tgt = make_absolute(tgtdir, pathname) for func, callback in preprocessors: if func(src): do = callback(srcdir, tgtdir, pathname) if do is not None: todo.append(do) for do in todo: do() def main(): srcdir = sys.argv[1] tgtdir = sys.argv[2] preprocess(srcdir, tgtdir) if __name__ == '__main__': main()