coursebuilder/coursebuilder/__main__.py
2023-11-12 20:23:04 +01:00

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()