262 lines
8.6 KiB
Python
262 lines
8.6 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
CourseBuilder
|
|
|
|
Coursebuilder is a preprocessor tool for [pandoc](https://pandoc.org)
|
|
to generate multi-lingual curricula documentation tables from
|
|
structured representations as a flatfile database. Data scheme and
|
|
actual values are kept in YAML files in order to version them with git.
|
|
|
|
"""
|
|
|
|
from argparse import ArgumentParser
|
|
import itertools
|
|
import yaml
|
|
import textwrap
|
|
import string
|
|
import os
|
|
|
|
|
|
class MarkdownGenerator:
|
|
def __init__(self) -> None:
|
|
pass
|
|
|
|
|
|
def generate_markdown(self,ti,pagebreak = False,title = False,header_level = 1) -> str:
|
|
|
|
line_length = 128
|
|
column_ratio= 0.28
|
|
|
|
h_len = int(line_length * column_ratio)
|
|
d_len = line_length-h_len
|
|
|
|
if title:
|
|
print('#' * header_level,ti[0][1],'\n')
|
|
|
|
print(''.join(['+',"".ljust(h_len,'-'),'+',"".ljust(d_len,'-'),'+']))
|
|
|
|
headline = False
|
|
|
|
#
|
|
# this implements a Markdown Grid-Table
|
|
#
|
|
|
|
for k,v in ti:
|
|
|
|
if v == None:
|
|
v = ''
|
|
|
|
# row head
|
|
h = textwrap.wrap(k, h_len, break_long_words=False)
|
|
wrapper = textwrap.TextWrapper(d_len,break_long_words=True,replace_whitespace=False)
|
|
|
|
# split test to wrap lines into correct length
|
|
t = [wrapper.wrap(line) for line in v.split('\n')]
|
|
|
|
|
|
# replace empty arrays from split with a empty string
|
|
t = list(map(lambda e: [""] if e == [] else e, t))
|
|
|
|
# zip items of list
|
|
t = list(itertools.chain.from_iterable(t))
|
|
|
|
# get rows
|
|
rows = list(itertools.zip_longest(h,t,fillvalue=""))
|
|
|
|
# expand rows
|
|
for r in rows:
|
|
# insider recognize this as the computational dump(b)ness feature
|
|
if '<!-- tablebreak -->' in r[0] or '<!-- tablebreak -->' in r[1]:
|
|
print(''.join(['+',"".ljust(h_len,'-'),'+',"".ljust(d_len,'-'),'+']))
|
|
else:
|
|
print(''.join(['|',r[0].ljust(h_len,' '),'|',r[1].ljust(d_len,' '),'|']))
|
|
|
|
if headline:
|
|
print(''.join(['+',"".ljust(h_len,'-'),'+',"".ljust(d_len,'-'),'+']))
|
|
else:
|
|
print(''.join(['+',"".ljust(h_len,'='),'+',"".ljust(d_len,'='),'+']))
|
|
headline = True
|
|
|
|
|
|
# to control pagebreaks for pandoc
|
|
if pagebreak:
|
|
print('\n\\newpage')
|
|
|
|
class CourseBuilder:
|
|
|
|
def __init__(self) -> None:
|
|
self.__schema = None
|
|
|
|
|
|
def set_schema(self,schema = None):
|
|
self.__schema = schema
|
|
|
|
def get_template(self,field,lang='de'):
|
|
if 'template' in self.__schema[field]:
|
|
return self.__schema[field]['template'][lang]
|
|
else:
|
|
return "$value"
|
|
|
|
def is_translatable(self,field):
|
|
if 'translatable' in self.__schema[field]:
|
|
return self.__schema[field]['translatable']
|
|
else:
|
|
return True
|
|
|
|
def needs_spec(self,field):
|
|
if 'spec' in self.__schema[field]:
|
|
return self.__schema[field]
|
|
else:
|
|
return False
|
|
|
|
def process_label(self,field,lang='de'):
|
|
# processes the label of a field item
|
|
return self.__schema[field]['label'][lang]
|
|
|
|
def process_str(self,meta,field,lang='de'):
|
|
if self.is_translatable(field):
|
|
|
|
return [self.process_label(field,lang),meta[field][lang]]
|
|
else:
|
|
if not 'value' in meta[field]:
|
|
raise AssertionError(field,'incomplete')
|
|
|
|
return [self.process_label(field,lang),meta[field]['value']]
|
|
|
|
def process_enum(self,meta,field,lang='de'):
|
|
"""
|
|
enum have a specification 'specs' option
|
|
that can be forced by the scheme
|
|
"""
|
|
vv = meta[field]['value']
|
|
enum_val = self.__schema[field]['values'][vv][lang]
|
|
|
|
if self.needs_spec(field):
|
|
|
|
t = string.Template(self.get_template(field=field,lang=lang))
|
|
|
|
spec = meta[field]['spec'][lang]
|
|
|
|
return [self.process_label(field,lang),t.substitute({'value': enum_val,'spec': spec})]
|
|
else:
|
|
return [self.process_label(field,lang),enum_val]
|
|
|
|
def process_int(self,meta,field,lang='de'):
|
|
v = meta[field]['value']
|
|
t = string.Template(self.get_template(field,lang))
|
|
return [self.process_label(field,lang),t.substitute({'value' : v})]
|
|
|
|
def process_multikey(self,meta,field,lang='de'):
|
|
"""
|
|
multikey need to assign a numeric value to a key
|
|
"""
|
|
vs = meta[field]['value']
|
|
t = string.Template(self.get_template(field,lang))
|
|
|
|
k = self.process_label(field,lang)
|
|
|
|
parts = []
|
|
|
|
for e in vs:
|
|
kk = self.__schema[field]['keys'][e][lang]
|
|
parts.append(t.substitute({'key': kk, 'value' : vs[e]}))
|
|
|
|
return [k,', '.join(parts)]
|
|
|
|
|
|
def process(self,meta,fields = [],lang = 'de',pagebreak = False,createTitle=False,header_level=1):
|
|
|
|
table_items = []
|
|
|
|
for field in fields:
|
|
match self.__schema[field]['type']:
|
|
case 'str': table_items.append(self.process_str(meta,field,lang))
|
|
case 'enum': table_items.append(self.process_enum(meta,field,lang))
|
|
case 'int': table_items.append(self.process_int(meta,field,lang))
|
|
case 'multikey': table_items.append(self.process_multikey(meta,field,lang))
|
|
|
|
mdg = MarkdownGenerator()
|
|
mdg.generate_markdown(table_items,pagebreak,createTitle,header_level=header_level)
|
|
|
|
def process_book_section(self,section,lang='de'):
|
|
pass
|
|
|
|
def process_book(self,book,bookpath,create_title,pagebreak,lang='de',header_level=2):
|
|
|
|
actual_fields = []
|
|
|
|
for bi in book['book']:
|
|
if 'fields' in bi:
|
|
actual_fields = bi['fields']
|
|
if 'sections' in bi:
|
|
for section in bi['sections']:
|
|
if 'text' in section:
|
|
print(section['text'][lang])
|
|
if 'modules' in section:
|
|
for m in section['modules']:
|
|
mod_path = os.path.join(os.path.dirname(bookpath),m)
|
|
|
|
with open(mod_path) as fm:
|
|
self.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=lang,pagebreak=pagebreak,createTitle=create_title,header_level=header_level)
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
# arguments
|
|
parser = ArgumentParser(description='versatile curricula generator')
|
|
|
|
parser.add_argument('-m','--meta',action="extend", nargs="+", type=str,help="course description(s) as YAML file(s)")
|
|
parser.add_argument('-l','--lang',help="Language to parse from meta file (use de or en)",default='de')
|
|
parser.add_argument('-f','--fields',help="Fields to be used, the table will be build accordingly",action="extend", nargs="+", type=str)
|
|
parser.add_argument('-s','--schema',help="using provided schema")
|
|
parser.add_argument('-p','--pagebreak',action="store_true",help="add a pagebreak after each module")
|
|
parser.add_argument('-t','--title',action="store_true",help="take first value in list as title")
|
|
parser.add_argument('-b','--book',type=str,help="process a whole curriculum book with sections")
|
|
parser.add_argument('--level',type=int,default=1,help="level of header tags")
|
|
|
|
|
|
# get arguments
|
|
args = parser.parse_args()
|
|
|
|
# book mode with predefined setting from a book file
|
|
if args.book and args.schema:
|
|
|
|
cb = CourseBuilder()
|
|
|
|
with open(args.schema) as sf:
|
|
cb.set_schema(yaml.load(sf,Loader=yaml.Loader))
|
|
|
|
with open(args.book) as bf:
|
|
cb.process_book(yaml.load(bf,Loader=yaml.Loader),os.path.abspath(args.book),lang=args.lang,pagebreak=args.pagebreak,create_title=args.title,header_level=args.level)
|
|
|
|
|
|
# verbose command line mode
|
|
elif args.schema and args.meta and len(args.fields) > 0:
|
|
|
|
cb = CourseBuilder()
|
|
|
|
actual_fields = []
|
|
|
|
if os.path.isfile(args.fields[0]):
|
|
with open(args.fields[0]) as ff:
|
|
actual_fields = yaml.load(ff,Loader=yaml.Loader)['fields']
|
|
else:
|
|
actual_fields = args.fields
|
|
|
|
|
|
with open(args.schema) as f:
|
|
cb.set_schema(yaml.load(f,Loader=yaml.Loader))
|
|
|
|
for m in args.meta:
|
|
with open(m) as fm:
|
|
cb.process(yaml.load(fm,Loader=yaml.Loader),fields=actual_fields,lang=args.lang,pagebreak=args.pagebreak,createTitle=args.title,header_level=args.level)
|
|
else:
|
|
parser.print_help()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|