#!/usr/bin/python3 # -*- coding:utf-8 -*- import argparse import re import random import time import subprocess from os import path, makedirs, listdir from jinja2 import Environment, FileSystemLoader import pygit2 LICENSES = {'CC-by-sa-2.0': 'license-cc-by-sa-2.0', } TEMPLATES = { 'beamer': {'fragment': 'frame.tex', 'fragment_pratique': 'frame-pratique.tex', 'fragment_corrige': 'frame-corrige.tex', 'master': 'main-beamer.tex'}, 'article': {'fragment': 'fragment.tex', 'fragment_pratique': 'fragment-pratique.tex', 'fragment_corrige': 'fragment-corrige.tex', 'master': 'main-article.tex'}, 'questionnaire': {'fragment': 'fragment.tex', 'fragment_pratique': 'fragment-pratique.tex', 'fragment_corrige': 'fragment-corrige.tex', 'master': 'main-questionnaire.tex'} } LATEX_SUBS = [(re.compile('_'), '\\_'), ] DOCUMENTCLASS_RE = re.compile(r'\\documentclass\{(?P.+?)\}') SKBCONFIG_RE = re.compile(r'\\skbconfig\[\n\s*root\s*=\s*(?P.*),\n\s*rep\s*=\s*(?P.*),\n\s*pub\s*=\s*(?P.*),\n\s*fig\s*=\s*(?P.*),\n\s*sli\s*=\s*(?P.*),\n\s*acr\s*=\s*(?P.*),\n\s*bib\s*=\s*(?P.*)\n\s*\]\{skblocal.tex\}', re.M) SKBINPUT_RE = re.compile(r'[^%]\\skbinput\[from=(?P.*?)(,.*)?\]\{(?P.*?)\}', re.M) def get_unique_name(base): now = time.localtime() year = str(now[0]) month = str(now[1]).rjust(2, '0') day = str(now[2]).rjust(2, '0') rand = str(random.randint(0, 100)).rjust(2, '0') return '-'.join([base, year, month, day, rand]).decode('utf-8') def escape_tex(value): newval = value for pattern, replacement in LATEX_SUBS: newval = pattern.sub(replacement, newval) return newval def normalize_branch(args): if 'master' in args: return path.join('xelatex', path.dirname(args.master)) elif 'directory' in args: return path.join('xelatex', args.directory) else: raise Exception('No sufficient information to create branch') def git_integration(func): def commit_into_master(paths, repo, comment): author = repo.default_signature committer = author repo.checkout('refs/heads/master') index = repo.index for fp in paths: index.add(fp) index.write() tree = index.write_tree() master_ref = repo.references['refs/heads/master'] parents = [master_ref.peel().hex] master_head = repo.create_commit('refs/heads/master', author, committer, comment, tree, parents) return master_head def commit_into_branch(paths, branch, repo, comment): author = repo.default_signature committer = author repo.checkout(f'refs/heads/{branch}') index = repo.index for fp in paths: index.add(fp) index.write() tree = index.write_tree() branch_ref = repo.references[f'refs/heads/{branch}'] parents = [branch_ref.peel().hex] branch_commit = repo.create_commit(f'refs/heads/{branch}', author, committer, comment, tree, parents) return branch_commit def merge_master_into_branch(master_commit, branch, repo): author = repo.default_signature committer = author repo.checkout(f'refs/heads/{branch}') repo.merge(master_commit) tree = repo.index.write_tree() merge_commit = repo.create_commit('HEAD', author, committer, 'Merge master into xelatex/*', tree, [repo.head.target, master_commit]) def inner(args): try: repo = pygit2.Repository('./') except: repo = None if repo: branch_name = normalize_branch(args) if not branch_name in repo.branches.local: master_ref = repo.references['refs/heads/master'] commit = master_ref.peel() repo.branches.local.create(branch_name, commit) func(args) if repo: repo_status = repo.status() to_add_status = [pygit2.GIT_STATUS_WT_NEW, pygit2.GIT_STATUS_WT_MODIFIED] branch_add_paths = [fp for fp in repo_status if fp.startswith(f'presentations/') and repo_status[fp] in to_add_status] master_add_paths = [fp for fp in repo_status if (fp.startswith('content/') or fp.startswith('slides/')) and repo_status[fp] in to_add_status] if func.__name__ == 'init': comment = 'Initialisation de la formation' elif func.__name__ == 'update': comment = 'Construction de la formation' elif func.__name__ == 'outline': comment = 'Mise à jour du programme' else: comment = 'Travail sur la formation' if repo.branches['master'].is_checked_out(): if master_add_paths: master_head = commit_into_master(master_add_paths, repo, comment) else: master_head = repo.revparse_single('refs/heads/master') if branch_add_paths: commit_into_branch(branch_add_paths, branch_name, repo, comment) elif repo.branches[branch_name].is_checked_out(): if branch_add_paths: commit_into_branch(branch_add_paths, branch_name, repo, comment) if master_add_paths: master_head = commit_into_master(master_add_paths, repo, comment) else: master_head = repo.revparse_single('refs/heads/master') branch_ref = repo.references[f'refs/heads/{branch_name}'] if master_head not in repo.walk(branch_ref.target): merge_master_into_branch(master_head, branch_name, repo) return inner def main(): @git_integration def init(args): """ init function """ def get_institutes_logos(institutes_list=None): if not institutes_list: return [] institutes_logos = [] known_logos = {path.splitext(path.basename(l))[0]:l for l in listdir('./figures/logos')} for institute in institutes_list: if institute in known_logos: institutes_logos.append(known_logos[institute]) else: print(f'Unknown institute {institute}') print(f'Replacing with missing.png') institutes_logos.append('missing.png') return institutes_logos root = '../' if args.directory: root = root + re.sub(r'[\w-]+/?', '../', args.directory) name = 'diaporama.tex' title = args.title if not title: title = 'FIXME' author = args.author if not author: author = 'Cadoles' client = args.client if not client: client = 'FIXME' institutes = get_institutes_logos(args.institutes) logos_count = len(institutes) + 1 directory = args.directory if not directory: directory = '' license = LICENSES.get(args.license, 'license-cc-by-sa-2.0') document_class = args.format content = 'sli' if document_class == 'beamer' else 'rep' env = {'root': root, 'class': document_class, 'content': content, 'title': title, 'author': author, 'client': client, 'license': license, 'institutes': institutes, 'logos_count': logos_count} master = TEMPLATES[document_class]['master'] master_dir = path.join('presentations', directory) programme_dir = path.join(master_dir, 'programme') resources = [(master_dir, master), (master_dir, 'programme.tex'), (master_dir, 'support.tex'), (programme_dir, 'contenu.tex'), (programme_dir, 'duree.tex'), (programme_dir, 'evaluation.tex'), (programme_dir, 'moyens.tex'), (programme_dir, 'objectifs.tex'), (programme_dir, 'prerequis.tex'), (programme_dir, 'public.tex'), ] for directory, template_file in resources: template = jinja_env.get_template(template_file) rendered_template = template.render(**env) if not path.exists(directory): makedirs(directory) template_dest_name = name if template_file == master else template_file with open(path.join(directory, template_dest_name), 'w') as rendered_file: rendered_file.write(rendered_template) @git_integration def update(args): """ update function """ master_file = path.join('presentations', args.directory, 'diaporama.tex') with open(master_file, 'r') as master: tex_master = master.read() tex_class = DOCUMENTCLASS_RE.search(tex_master) tex_skbconfig = SKBCONFIG_RE.search(tex_master) tex_skbinputs = SKBINPUT_RE.finditer(tex_master) fragment = TEMPLATES[tex_class.group('document_class')]['fragment'] fragment_pratique = TEMPLATES[tex_class.group('document_class')]['fragment_pratique'] fragment_corrige = TEMPLATES[tex_class.group('document_class')]['fragment_corrige'] for skbinput in tex_skbinputs: rep = path.dirname(skbinput.group('tex')) rep = path.join(tex_skbconfig.group(skbinput.group('rep')), rep) tex_name = path.basename(skbinput.group('tex')) basename = '{0}.tex'.format(tex_name) dest = path.join(rep, basename) if not path.isfile(dest): print(dest) if not path.isdir(rep): makedirs(rep) if tex_name.endswith('-pratique'): template = jinja_env.get_template(fragment_pratique) elif tex_name.endswith('-corrige'): template = jinja_env.get_template(fragment_corrige) else: template = jinja_env.get_template(fragment) env = {'title': basename, 'subtitle': '', 'name': dest} rendered_template = template.render(**env) with open(dest, 'w') as rendered_file: rendered_file.write(rendered_template) def tex_compile(args): master_files = [path.join('presentations', args.directory, tex_file) for tex_file in ['diaporama.tex', 'programme.tex', 'support.tex']] for master_file in master_files: subprocess.call(['rubber', '--inplace', '-c shell_escape', '--unsafe', '--module=xelatex', master_file]) @git_integration def outline(args): """ outline creation """ part_level = 0 section_level = 1 subsection_level = 2 frametitle_level = 3 framesubtitle_level = 4 def file_path_from_skbinput(skbinput_re, master, skbconfig): rel_path = path.join(skbconfig.group('root'), skbconfig.group(skbinput_re.group('rep')), skbinput_re.group('tex')) + '.tex' root_path = path.abspath(path.dirname(master)) return path.normpath(path.join(root_path, rel_path)) def reorder_lists(*args): reordered_list = [] for l in args: reordered_list.extend(l) reordered_list.sort(key=lambda x: x[0]) return reordered_list def outline_from_include(include, start, document_class): frametitle_re = re.compile(r'\\frametitle\{(?P.*?)\}') framesubtitle_re = re.compile(r'\\framesubtitle\{(?P.*?)\}') skbheading_re = re.compile(r'\\skbheading\{(?P.*?)\}') with open(include, 'r') as include_fh: content = include_fh.read() if document_class == 'beamer': frametitles = frametitle_re.finditer(content) framesubtitles = framesubtitle_re.finditer(content) frametitles_list = [(ft.start(), frametitle_level, ft.group('name')) for ft in frametitles] framesubtitles_list = [(fs.start(), framesubtitle_level, fs.group('name')) for fs in framesubtitles] frame_list = reorder_lists(frametitles_list, framesubtitles_list) if frame_list: div = int('1{}'.format('0'*len(str(frame_list[-1][0])))) return [(start + f[0]/div, f[1], f[2]) for f in frame_list] else: return [] def filter_outlines(headers_list, max_level=0): filtered_outlines = [] default_max_level = max([hl[1] for hl in headers_list]) if not max_level: max_level = default_max_level temp_max_level = default_max_level buffered_header = {l: None for l in range(max_level + 1)} filtered_out = ['Pratique', 'Plan', 'Licence du document'] for header in headers_list: if header[1] <= min(max_level, default_max_level, temp_max_level): if header[2] in filtered_out: temp_max_level = header[1] + 1 continue elif header[2] != buffered_header[header[1]]: buffered_header[header[1]] = header[2] for bf in buffered_header: if bf > header[1]: buffered_header[bf] = None filtered_outlines.append(header) temp_max_level = default_max_level return filtered_outlines def outline_format(headers_list): levels = list(set([hl[1] for hl in headers_list])) levels.sort() flattened_levels = {l: levels.index(l) for l in levels} for header in headers_list: print('{}{}'.format('\t' * flattened_levels[header[1]], header[2])) def render_outline(header_list, master): item = "\\item {content}" itemize = "\\begin{{itemize}}\n{content}\n\\end{{itemize}}" content_file = path.join(path.dirname(path.abspath(master)), 'programme', 'contenu.tex') with open(content_file, 'w') as content_fh: content_fh.write(itemize.format(content='\n'.join([item.format(content=c[2]) for c in header_list]))) def structure_outline(header_list): root = Outline('Contenu') current_outline = root for header in header_list: if header[1] > current_outline.level: parent = current_outline elif header[1] == current_outline.level: parent = current_outline.get_parent() elif header[1] == current_outline.level - 1: parent = current_outline.get_parent().get_parent() else: parent = root current_outline = Outline(header[2], parent, header[1]) parent.add_child(current_outline) return root section_re = re.compile(r'\\section\{(?P.*?)\}') part_re = re.compile(r'\\part\{(?P.*?)}') subsection_re = re.compile(r'\\subsection\{(?P.*?)\}') master_file = path.join('presentations', args.directory, 'diaporama.tex') max_level = args.levels with open(master_file, 'r') as master_tex: master = master_tex.read() skbconfig = SKBCONFIG_RE.search(master) document_class = DOCUMENTCLASS_RE.search(master).group('document_class') parts = part_re.finditer(master) sections = section_re.finditer(master) subsections = subsection_re.finditer(master) includes = SKBINPUT_RE.finditer(master) parts_list = [(part.start(), part_level, part.group('name')) for part in parts] sections_list = [(section.start(), section_level, section.group('name')) for section in sections] includes_list = [element for skbinput in includes for element in outline_from_include(file_path_from_skbinput(skbinput, master_file, skbconfig), skbinput.start(), document_class)] subsections_list = [(subsection.start(), subsection_level, subsection.group('name')) for subsection in subsections] structured_outline = structure_outline(filter_outlines(reorder_lists(parts_list, sections_list, includes_list, subsections_list), max_level=max_level)) content_file = path.join(path.dirname(path.abspath(master_file)), 'programme', 'contenu.tex') with open(content_file, 'w') as content_fh: content_fh.write(structured_outline.render()) jinja_loader = FileSystemLoader('./templates') jinja_env = Environment(loader=jinja_loader, block_start_string='((*', block_end_string='*))', variable_start_string='(((', variable_end_string=')))', comment_start_string='((=', comment_end_string='=))', trim_blocks=True) jinja_env.filters['escape_tex'] = escape_tex parser = argparse.ArgumentParser(description="Préparation des fichiers tex") subparsers = parser.add_subparsers(help='Aide des sous-commandes') parser_init = subparsers.add_parser('init', help='Initialisation du fichier maître') parser_init.add_argument('-f', '--format', help="Format du document", required=True) parser_init.add_argument('-a', '--author', help="Auteur de la formation") parser_init.add_argument('-c', '--client', help="Client") parser_init.add_argument('-t', '--title', help="Titre de la formation") parser_init.add_argument('-l', '--license', help="Termes de mise à disposition de la formation") parser_init.add_argument('-d', '--directory', help="Sous-répertoires où créer le fichier", required=True) parser_init.add_argument('-i', '--institutes', nargs='*', help="Instituts dont les logos sont requis") parser_init.set_defaults(func=init) parser_update = subparsers.add_parser('update', help='Mise à jour des fichiers inclus') parser_update.add_argument('-d', '--directory', help="Sous-répertoires contenant le diaporama", required=True) parser_update.set_defaults(func=update) parser_outline = subparsers.add_parser('outline', help="Création du programme à partir du fichier maître") parser_outline.add_argument('-d', '--directory', help="Sous-répertoires contenant le diaporama", required=True) parser_outline.add_argument('-l', '--levels', help="Niveaux de titre à inclure dans le plan", type=int, default=0) parser_outline.set_defaults(func=outline) if subprocess.check_output(['rubber', '--version']): parser_compile = subparsers.add_parser('compile', help='Compiler les différents documents en faisant appel à rubber') parser_compile.add_argument('-d', '--directory', help="Sous-répertoires contenant les documents", required=True) parser_compile.set_defaults(func=tex_compile) args = parser.parse_args() if hasattr(args, 'func'): args.func(args) else: parser.print_usage() class Outline: item = "{indent}\\item {content}" itemize = "\n{indent}\\begin{{itemize}}\n{content}\n{indent}\\end{{itemize}}" def __init__(self, content, parent=None, level=-1): self.children = [] self.parent = parent self.level = level self.content = content def is_leaf(self): if self.children: return False return True def add_child(self, child): self.children.append(child) def get_children(self): return self.children def get_parent(self): return self.parent def render(self): if self.get_parent(): rendered = self.item.format(indent=' '*self.level, content=self.content) else: rendered = '' if not self.is_leaf(): rendered += self.itemize.format(indent=' '*self.level, content='\n'.join([c.render() for c in self.get_children()])) return rendered def __repr__(self): return f"" if __name__ == '__main__': main()