diff --git a/.gitignore b/.gitignore index 912a858..4d2e6dc 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,4 @@ dmypy.json # onpoint-specific test/slides.*.html - +**/.onpoint/ \ No newline at end of file diff --git a/cache.py b/cache.py new file mode 100644 index 0000000..950bec9 --- /dev/null +++ b/cache.py @@ -0,0 +1,77 @@ +import os +import time + +import helper + +timestamp_file_name = "last-compile.txt" + +# Check whether we are forced to recompile the entire project. +# This is the case if: +# (a) the project was not compiled yet or +# (b) meta.yml was changed since the last compile or +# (c) the layouts have been changed since the last compile +# The current time is saved to the cache folder for later use. +def is_recompile_obligatory(root, lang): + if not os.path.isdir(os.path.join(root, ".onpoint", lang)): + # case (a) + __init_cache_directory(root, lang) + print("First compile, no caches available.") + save_compile_timestamp(root, lang) + return True + if file_touched_since_last_compile(root, lang, "meta.yml"): + # case (b) + print("meta.yml changed since the last compilation, forced to recompile.") + return True + + for file in os.listdir(os.path.join(root, 'layouts')): + if os.path.isfile(os.path.join(root, 'layouts', file)): + layout_path = os.path.join('layouts', file) + if file_touched_since_last_compile(root, lang, layout_path): + # case (c) + print("Templates changed since the last compilation, forced to recompile.") + return True + return False + +# Check if some file was touched since the last compile. +def file_touched_since_last_compile(root, lang, file): + file_path = os.path.join(root, file) + modification_time = helper.get_modification_time(file_path) + compilation_time = __get_last_compile_time(root, lang) + return modification_time > compilation_time + +# Check whether a cached version of this file exists. +def not_cached_before(root, lang, file): + file_path = os.path.join(root, '.onpoint', lang, file + '.cache') + return not os.path.exists(file_path) + +# Restore the cached chapter. +def get_cached_chapter(root, lang, chapter): + chapter_path = os.path.join(root, '.onpoint', lang, chapter + '.cache') + return open(chapter_path, 'r').read() + +# Store a cached version of a chapter. +def store_cached_chapter(root, lang, chapter, content): + chapter_path = os.path.join(root, '.onpoint', lang, chapter + '.cache') + chapter_file = open(chapter_path, 'w') + chapter_file.write(content) + chapter_file.close() + +# Initialize the cache directory '.onpoint' in the project's root folder. +# It will contain a file storing the last compile time and a directory +# for each language. The latter will have cached versions of each chapter. +def __init_cache_directory(root, lang): + cache_dir = os.path.join(root, '.onpoint', lang) + os.makedirs(cache_dir) + +# Save the current time to the timestamp file. +def save_compile_timestamp(root, lang, file_name=timestamp_file_name): + timestamp_file_path = os.path.join(root, '.onpoint', lang, file_name) + timestamp_file = open(timestamp_file_path, 'w') + timestamp_file.write(str(int(time.time()))) + timestamp_file.close() + +# Retrieve the compile timestamp. +def __get_last_compile_time(root, lang): + timestamp_file_path = os.path.join(root, '.onpoint', lang, timestamp_file_name) + with open(timestamp_file_path) as f: + return int(f.readline()) diff --git a/chapters.py b/chapters.py index fbbba45..04cded5 100644 --- a/chapters.py +++ b/chapters.py @@ -1,5 +1,6 @@ import os +import cache import helper import slides as slides_helper @@ -8,7 +9,7 @@ lang = "" # Compiles a presentation's chapters (the content without the wrapper) # according to slides.yml and returns them as one string of
s. -def compile_chapters(root_directory, language): +def compile_chapters(root_directory, language, force_recompile=False): global root root = root_directory @@ -16,23 +17,43 @@ def compile_chapters(root_directory, language): lang = language structure = helper.read_yaml(root, 'slides.yml') + no_cache = cache.is_recompile_obligatory(root, lang) or force_recompile + chapters = "" for chapter in structure: - chapters += '
\n' + compile_chapter(chapter) + '\n
' + chapters += '
\n' + compile_chapter(chapter, no_cache) + '\n
' + + cache.save_compile_timestamp(root, lang) return chapters # string # Compiles a presentation's chapter (given its name) by splitting it into # slides, compiling those, and returning them as a string of
s. -def compile_chapter(chapter): +def compile_chapter(chapter, no_cache): global root if type(chapter) != str: raise Exception('Chapters with attributes are not supported yet.\n' + str(chapter)) - chapter_file = open(os.path.join(root, 'slides/' + chapter + '.' + lang + '.md'), 'r') + + chapter_file_name = 'slides/' + chapter + '.' + lang + '.md' + chapter_path = os.path.join(root, chapter_file_name) + + can_use_cache = not ( + no_cache or + cache.not_cached_before(root, lang, chapter) or + cache.file_touched_since_last_compile(root, lang, chapter_file_name)) + + if can_use_cache: + print("Using cached version of chapter " + chapter + " (" + lang + ").") + return cache.get_cached_chapter(root, lang, chapter) + + print("Recompiling chapter " + chapter + " (" + lang + ").") + chapter_file = open(chapter_path, 'r') delimiter = '\n@slide' chapter_input = ('\n' + chapter_file.read()).split(delimiter) slides = [delimiter + slide for slide in chapter_input][1:] - chapter = '' + chapter_content = '' for slide in slides: - chapter += '
\n' + slides_helper.compile_slide(slide, root) + '\n
\n' - return chapter \ No newline at end of file + chapter_content += '
\n' + slides_helper.compile_slide(slide, root) + '\n
\n' + + cache.store_cached_chapter(root, lang, chapter, chapter_content) + return chapter_content \ No newline at end of file diff --git a/helper.py b/helper.py index f708630..f979184 100644 --- a/helper.py +++ b/helper.py @@ -1,5 +1,6 @@ -import yaml import os +import stat +import yaml # Calls a YAML parser for the given relative path inside the presentation root # directory. Returns a dictionary with the YAML content. @@ -24,4 +25,8 @@ def insert_metadata(content, root, lang): # print('replace', placeholder, 'with', filler) if '@' + key in content: content = content.replace(placeholder, filler) - return content \ No newline at end of file + return content + +# Retrieve the modification time for a given file. +def get_modification_time(path): + return os.stat(path)[stat.ST_MTIME] \ No newline at end of file diff --git a/main.py b/main.py index c84b9f9..4a26b75 100644 --- a/main.py +++ b/main.py @@ -10,9 +10,10 @@ import helper # Compiles a presentation in the given language from the given directory and # stores it in a corresponding slides.lang.html file inside the same directory. -def compile(root, language='en'): +def compile(root, language='en', force_recompile=False): wrapper = open(os.path.join(root, 'layouts/root.html'), 'r').read() - wrapper = wrapper.replace('@slides', chapters.compile_chapters(root, language)) + compiled_chapters = chapters.compile_chapters(root, language, force_recompile) + wrapper = wrapper.replace('@slides', compiled_chapters) wrapper = helper.insert_metadata(wrapper, root, language) wrapper = fragments.defragmentize(wrapper) with open(os.path.join(root, 'slides.' + language + '.html'), 'w+') as output: @@ -23,12 +24,14 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("rootdirectory", help="your project's root directory") parser.add_argument("-l", "--language", default="all", help="the presentation language (default: all)") + parser.add_argument("-f", "--force-recompile", action='store_true', help="recompiling the entire presentation without caches") args = parser.parse_args() + force_recompile = False or args.force_recompile if args.language == "all": for language in helper.get_available_languages(args.rootdirectory): - compile(args.rootdirectory, language=language) + compile(args.rootdirectory, language=language, force_recompile=force_recompile) else: - compile(args.rootdirectory, language=args.language) + compile(args.rootdirectory, language=args.language, force_recompile=force_recompile)