formations/setup_main_tex_file.py

498 lines
21 KiB
Python
Executable File

#!/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<document_class>.+?)\}')
SKBCONFIG_RE = re.compile(r'\\skbconfig\[\n\s*root\s*=\s*(?P<root>.*),\n\s*rep\s*=\s*(?P<rep>.*),\n\s*pub\s*=\s*(?P<pub>.*),\n\s*fig\s*=\s*(?P<fig>.*),\n\s*sli\s*=\s*(?P<sli>.*),\n\s*acr\s*=\s*(?P<acr>.*),\n\s*bib\s*=\s*(?P<bib>.*)\n\s*\]\{skblocal.tex\}', re.M)
SKBINPUT_RE = re.compile(r'[^%]\\skbinput\[from=(?P<rep>.*?)(,.*)?\]\{(?P<tex>.*?)\}', 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<name>.*?)\}')
framesubtitle_re = re.compile(r'\\framesubtitle\{(?P<name>.*?)\}')
skbheading_re = re.compile(r'\\skbheading\{(?P<name>.*?)\}')
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<name>.*?)\}')
part_re = re.compile(r'\\part\{(?P<name>.*?)}')
subsection_re = re.compile(r'\\subsection\{(?P<name>.*?)\}')
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"<Outline {self.content} - {self.level}>"
if __name__ == '__main__':
main()